Skip to content

Commit 7bc9544

Browse files
feat(assistant): displaying annotations
1 parent 8e937d4 commit 7bc9544

File tree

16 files changed

+183
-69
lines changed

16 files changed

+183
-69
lines changed

apps/spa/src/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import { routes } from './app.routes';
55
import { provideAnimations } from '@angular/platform-browser/animations';
66
import { provideHttpClient } from '@angular/common/http';
77
import { provideMarkdown } from 'ngx-markdown';
8+
import { AnnotationPipe } from './pipes/annotation.pipe';
89

910
export const appConfig: ApplicationConfig = {
1011
providers: [
1112
provideRouter(routes),
1213
provideAnimations(),
1314
provideHttpClient(),
1415
provideMarkdown(),
16+
AnnotationPipe,
1517
],
1618
};

apps/spa/src/app/components/chat/chat-audio/chat-audio.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@if (getMessageText) {
1+
@if (isAudioEnabled && message && (message | messageText)) {
22
<span class="chat-audio" [ngClass]="'chat-audio--' + message.role">
33
@if (!isStarted) {
44
<mat-icon (click)="speech()">play_circle</mat-icon>

apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,33 @@ import { Component, Input, OnInit } from '@angular/core';
22
import { MatIconModule } from '@angular/material/icon';
33
import { delay } from 'rxjs';
44
import { ChatAudioResponse, PostSpeechDto } from '@boldare/openai-assistant';
5-
import { NgClass } from '@angular/common';
6-
import { getMessageText } from '../../controls/message-content/message-content.helpers';
5+
import { CommonModule } from '@angular/common';
76
import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service';
87
import {
98
ChatMessage,
109
SpeechVoice,
1110
} from '../../../modules/+chat/shared/chat.model';
11+
import { MessageTextPipe } from '../../../pipes/message-text.pipe';
1212
import { environment } from '../../../../environments/environment';
1313

1414
@Component({
1515
selector: 'ai-chat-audio',
1616
standalone: true,
17-
imports: [MatIconModule, NgClass],
17+
imports: [MatIconModule, CommonModule, MessageTextPipe],
18+
providers: [MessageTextPipe],
1819
templateUrl: './chat-audio.component.html',
1920
styleUrl: './chat-audio.component.scss',
2021
})
2122
export class ChatAudioComponent implements OnInit {
2223
@Input() message!: ChatMessage;
2324
isStarted = false;
2425
audio = new Audio();
26+
isAudioEnabled = environment.isAudioEnabled;
2527

26-
get getMessageText(): string {
27-
if (!environment.isAudioEnabled || !this.message) {
28-
return '';
29-
}
30-
31-
return getMessageText(this.message);
32-
}
33-
34-
constructor(private readonly chatService: ChatClientService) {}
28+
constructor(
29+
private readonly chatService: ChatClientService,
30+
private readonly messageTextPipe: MessageTextPipe,
31+
) {}
3532

3633
ngOnInit(): void {
3734
this.audio.onended = this.onEnded.bind(this);
@@ -50,7 +47,9 @@ export class ChatAudioComponent implements OnInit {
5047
}
5148

5249
speech(): void {
53-
if (!this.getMessageText) {
50+
const content = this.messageTextPipe.transform(this.message);
51+
52+
if (!content) {
5453
return;
5554
}
5655

@@ -62,7 +61,7 @@ export class ChatAudioComponent implements OnInit {
6261
}
6362

6463
const payload: PostSpeechDto = {
65-
content: getMessageText(this.message),
64+
content,
6665
voice: SpeechVoice.Onyx,
6766
};
6867

apps/spa/src/app/components/chat/chat-message/chat-message.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44
}
55

66
<div class="chat-message">
7-
@if (messageText) {
8-
<span markdown [data]="messageText"></span>
7+
@if (message | messageText) {
8+
<span markdown [data]="message | messageText"></span>
99
}
1010

1111
@if (message.role !== chatRole.System) {
1212
<ai-chat-audio [message]="message"></ai-chat-audio>
1313
}
1414

15-
@if (messageImage.length) {
15+
@if ((message | messageImageFile).length) {
1616
<div class="chat-message__file">
17-
@for (image of messageImage; track messageImage) {
17+
@for (image of message | messageImageFile; track image.image_file.file_id) {
1818
<div>File ID: {{ image.image_file.file_id }}</div>
1919
}
2020
</div>

apps/spa/src/app/components/chat/chat-message/chat-message.component.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import { MarkdownComponent } from 'ngx-markdown';
77
import { ChatAudioComponent } from '../chat-audio/chat-audio.component';
88
import { NgClass } from '@angular/common';
99
import { ChatAvatarComponent } from '../chat-avatar/chat-avatar.component';
10-
import {
11-
getMessageImage,
12-
getMessageText,
13-
} from '../../controls/message-content/message-content.helpers';
14-
import { ImageFileContentBlock } from 'openai/resources/beta/threads';
10+
import { MessageTextPipe } from '../../../pipes/message-text.pipe';
11+
import { MessageImageFilePipe } from '../../../pipes/message-file.pipe';
12+
import { AnnotationPipe } from '../../../pipes/annotation.pipe';
1513

1614
@Component({
1715
selector: 'ai-chat-message',
@@ -23,21 +21,15 @@ import { ImageFileContentBlock } from 'openai/resources/beta/threads';
2321
MarkdownComponent,
2422
ChatAudioComponent,
2523
ChatAvatarComponent,
24+
MessageTextPipe,
25+
MessageImageFilePipe,
2626
],
2727
})
2828
export class ChatMessageComponent {
2929
@Input() message!: ChatMessage;
3030
@Input() class = '';
3131
chatRole = ChatRole;
3232

33-
get messageText(): string {
34-
return getMessageText(this.message);
35-
}
36-
37-
get messageImage(): ImageFileContentBlock[] {
38-
return getMessageImage(this.message);
39-
}
40-
4133
@HostBinding('class') get getClasses(): string {
4234
return `${this.class} is-${this.message?.role || 'none'}`;
4335
}

apps/spa/src/app/components/controls/message-content/message-content.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
22
import { MatIcon } from '@angular/material/icon';
33
import { toSignal } from '@angular/core/rxjs-interop';
4-
import { MessageContentService } from './message-content.service';
54
import { ControlItemComponent } from '../control-item/control-item.component';
65
import { ControlIconComponent } from '../control-icon/control-icon.component';
76
import { AiFilesDirective } from '../files/files.directive';
7+
import { MessageContentService } from './message-content.service';
88

99
@Component({
1010
selector: 'ai-message-content',
Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { TextContentBlock } from 'openai/resources/beta/threads/messages';
22
import { ImageFileContentBlock } from 'openai/src/resources/beta/threads/messages';
3-
import { ChatMessage } from '../../../modules/+chat/shared/chat.model';
43

54
export function isTextContentBlock(item: {
65
type: string;
@@ -13,25 +12,3 @@ export function isImageFileContentBlock(item: {
1312
}): item is ImageFileContentBlock {
1413
return item.type === 'image_file';
1514
}
16-
17-
export const getMessageText = (message: ChatMessage): string => {
18-
if (typeof message.content === 'string') {
19-
return message.content;
20-
}
21-
22-
// @TODO: handle all types of message content
23-
return message.content
24-
.filter(isTextContentBlock)
25-
.map(block => block.text.value)
26-
.join(' ');
27-
};
28-
29-
export const getMessageImage = (
30-
message: ChatMessage,
31-
): ImageFileContentBlock[] => {
32-
if (typeof message.content === 'string') {
33-
return [];
34-
}
35-
36-
return message.content.filter(isImageFileContentBlock);
37-
};

apps/spa/src/app/modules/+chat/shared/chat-gateway.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChatEvents } from './chat.model';
33
import io from 'socket.io-client';
44
import {
55
ChatCallDto,
6+
MessageWithAnnotations,
67
TextCreatedPayload,
78
TextDeltaPayload,
89
TextDonePayload,
@@ -37,7 +38,7 @@ export class ChatGatewayService {
3738
return this.watchEvent(ChatEvents.TextDelta);
3839
}
3940

40-
textDone(): Observable<TextDonePayload> {
41+
textDone(): Observable<MessageWithAnnotations<TextDonePayload>> {
4142
return this.watchEvent(ChatEvents.TextDone);
4243
}
4344
}

apps/spa/src/app/modules/+chat/shared/chat.service.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Injectable } from '@angular/core';
1+
import { Injectable, Injector } from '@angular/core';
22
import {
33
BehaviorSubject,
44
distinctUntilChanged,
@@ -9,20 +9,21 @@ import {
99
take,
1010
tap,
1111
} from 'rxjs';
12-
import { OpenAiFile, GetThreadResponseDto } from '@boldare/openai-assistant';
13-
import { ChatRole, ChatMessage, ChatMessageStatus } from './chat.model';
14-
import { ChatGatewayService } from './chat-gateway.service';
15-
import { ChatClientService } from './chat-client.service';
16-
import { ThreadService } from './thread.service';
17-
import { ChatFilesService } from './chat-files.service';
18-
import { environment } from '../../../../environments/environment';
1912
import {
2013
ImageFileContentBlock,
2114
Message,
2215
MessageContent,
2316
Text,
2417
} from 'openai/resources/beta/threads/messages';
18+
import { OpenAiFile, GetThreadResponseDto } from '@boldare/openai-assistant';
19+
import { ChatRole, ChatMessage, ChatMessageStatus } from './chat.model';
20+
import { ChatGatewayService } from './chat-gateway.service';
21+
import { ChatClientService } from './chat-client.service';
22+
import { ThreadService } from './thread.service';
23+
import { ChatFilesService } from './chat-files.service';
2524
import { MessageContentService } from '../../../components/controls/message-content/message-content.service';
25+
import { environment } from '../../../../environments/environment';
26+
import { AnnotationPipe } from '../../../pipes/annotation.pipe';
2627

2728
@Injectable({ providedIn: 'root' })
2829
export class ChatService {
@@ -38,6 +39,7 @@ export class ChatService {
3839
private readonly threadService: ThreadService,
3940
private readonly chatFilesService: ChatFilesService,
4041
private readonly messageContentService: MessageContentService,
42+
private readonly annotationPipe: AnnotationPipe,
4143
) {
4244
document.body.classList.add('ai-chat');
4345

@@ -213,13 +215,13 @@ export class ChatService {
213215
}
214216

215217
watchTextDone(): Subscription {
216-
return this.chatGatewayService.textDone().subscribe(data => {
218+
return this.chatGatewayService.textDone().subscribe(event => {
217219
this.isTyping$.next(false);
218220
this.isResponding$.next(false);
219221
this.messages$.next([
220222
...this.messages$.value.slice(0, -1),
221223
{
222-
content: data.text.value,
224+
content: this.annotationPipe.transform(event),
223225
role: ChatRole.Assistant,
224226
},
225227
]);
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
import { MessageWithAnnotations } from '@boldare/openai-assistant';
3+
import { Message } from 'openai/resources/beta/threads';
4+
import { isTextContentBlock } from '../components/controls/message-content/message-content.helpers';
5+
6+
@Pipe({
7+
standalone: true,
8+
name: 'annotation'
9+
})
10+
export class AnnotationPipe implements PipeTransform {
11+
transform(event: MessageWithAnnotations<{ message: Message }>): string {
12+
if (!isTextContentBlock(event.data.message.content[0])) {
13+
return '';
14+
}
15+
16+
const { text } = event.data.message.content[0];
17+
18+
if (!event.annotations?.length) {
19+
return text.value;
20+
}
21+
22+
for (const item of event.annotations) {
23+
const { index, annotation } = item;
24+
text.value = text.value.replace(annotation.text, `[${index}]`);
25+
}
26+
27+
return text.value;
28+
}
29+
}

0 commit comments

Comments
 (0)