Skip to content

Commit

Permalink
Merge pull request #67 from boldare/feat/annotations
Browse files Browse the repository at this point in the history
Annotations - displaying numbers and dedicated section in the message
  • Loading branch information
sebastianmusial committed Jun 13, 2024
2 parents 8e937d4 + 42582b8 commit c2f8eec
Show file tree
Hide file tree
Showing 49 changed files with 675 additions and 195 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div
class="annotation"
(mouseover)="showDetails()"
[matTooltip]="fileDetails ? fileDetails.filename : 'Reading data...'"
matTooltipPosition="above">
[{{ index }}]
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import 'settings';

.annotation {
cursor: pointer;

&:hover {
background-color: rgba(0, 0, 0, 0.15);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChatAnnotationComponent } from './chat-annotation.component';
import { HttpClientTestingModule } from '@angular/common/http/testing';

describe('ChatAnnotationComponent', () => {
let component: ChatAnnotationComponent;
let fixture: ComponentFixture<ChatAnnotationComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChatAnnotationComponent, HttpClientTestingModule],
}).compileComponents();

fixture = TestBed.createComponent(ChatAnnotationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component, Input } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { Annotation } from 'openai/resources/beta/threads';
import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service';
import { FileObject } from 'openai/resources';
import { isFileCitation } from '../../../pipes/annotation.pipe';
import { take } from 'rxjs';

@Component({
selector: 'ai-chat-annotation',
standalone: true,
templateUrl: './chat-annotation.component.html',
styleUrl: './chat-annotation.component.scss',
imports: [MatTooltip],
})
export class ChatAnnotationComponent {
@Input() annotation!: Annotation;
@Input() index = 1;
fileDetails!: FileObject;

get fileId(): string {
return isFileCitation(this.annotation)
? this.annotation.file_citation.file_id
: this.annotation.file_path.file_id;
}

constructor(private chatClientService: ChatClientService) {}

showDetails() {
if (!this.fileId || !!this.fileDetails) {
return;
}

this.chatClientService
.retriveFile(this.fileId)
.pipe(take(1))
.subscribe(details => (this.fileDetails = details));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@if (message && message.type === 'text' && message.text.annotations.length) {
<div class="title">Annotations:</div>
<div class="content">
@for (
annotation of message.text.annotations;
track annotation.text + $index
) {
<ai-chat-annotation [annotation]="annotation" [index]="$index + 1">
[{{ $index + 1 }}]
</ai-chat-annotation>
}
</div>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@import 'settings';

:host {
display: flex;
align-items: baseline;
gap: $size-2;
border-top: 1px dashed var(--color-grey-500);
margin-top: $size-3;
padding-top: $size-3;
font-size: 12px;

&:empty {
display: none;
}
}

.title {
font-weight: 500;
}

.content {
display: flex;
gap: $size-1;
margin-top: $size-2;
}

.annotation {
cursor: pointer;

&:hover {
background-color: rgba(0, 0, 0, 0.15);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MarkdownModule } from 'ngx-markdown';
import { ChatAnnotationsComponent } from './chat-annotations.component';

describe('ChatAnnotationsComponent', () => {
let component: ChatAnnotationsComponent;
let fixture: ComponentFixture<ChatAnnotationsComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChatAnnotationsComponent, MarkdownModule.forRoot()],
}).compileComponents();

fixture = TestBed.createComponent(ChatAnnotationsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Component, Input } from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { MessageContent } from 'openai/resources/beta/threads';
import { ChatAnnotationComponent } from '../chat-annotation/chat-annotation.component';

@Component({
selector: 'ai-chat-annotations',
standalone: true,
templateUrl: './chat-annotations.component.html',
styleUrl: './chat-annotations.component.scss',
imports: [MatTooltip, ChatAnnotationComponent],
})
export class ChatAnnotationsComponent {
@Input() message!: MessageContent;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@if (getMessageText) {
@if (isAudioEnabled && message && (message | messageText)) {
<span class="chat-audio" [ngClass]="'chat-audio--' + message.role">
@if (!isStarted) {
<mat-icon (click)="speech()">play_circle</mat-icon>
Expand Down
27 changes: 13 additions & 14 deletions apps/spa/src/app/components/chat/chat-audio/chat-audio.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,33 @@ import { Component, Input, OnInit } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { delay } from 'rxjs';
import { ChatAudioResponse, PostSpeechDto } from '@boldare/openai-assistant';
import { NgClass } from '@angular/common';
import { getMessageText } from '../../controls/message-content/message-content.helpers';
import { CommonModule } from '@angular/common';
import { ChatClientService } from '../../../modules/+chat/shared/chat-client.service';
import {
ChatMessage,
SpeechVoice,
} from '../../../modules/+chat/shared/chat.model';
import { MessageTextPipe } from '../../../pipes/message-text.pipe';
import { environment } from '../../../../environments/environment';

@Component({
selector: 'ai-chat-audio',
standalone: true,
imports: [MatIconModule, NgClass],
imports: [MatIconModule, CommonModule, MessageTextPipe],
providers: [MessageTextPipe],
templateUrl: './chat-audio.component.html',
styleUrl: './chat-audio.component.scss',
})
export class ChatAudioComponent implements OnInit {
@Input() message!: ChatMessage;
isStarted = false;
audio = new Audio();
isAudioEnabled = environment.isAudioEnabled;

get getMessageText(): string {
if (!environment.isAudioEnabled || !this.message) {
return '';
}

return getMessageText(this.message);
}

constructor(private readonly chatService: ChatClientService) {}
constructor(
private readonly chatService: ChatClientService,
private readonly messageTextPipe: MessageTextPipe,
) {}

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

speech(): void {
if (!this.getMessageText) {
const content = this.messageTextPipe.transform(this.message);

if (!content) {
return;
}

Expand All @@ -62,7 +61,7 @@ export class ChatAudioComponent implements OnInit {
}

const payload: PostSpeechDto = {
content: getMessageText(this.message),
content,
voice: SpeechVoice.Onyx,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@
}

<div class="chat-message">
@if (messageText) {
<span markdown [data]="messageText"></span>
@for (msg of message.content; track $index) {
@if (msg.type === 'text') {
<span markdown [data]="msg | annotation"></span>
}
<ai-chat-annotations [message]="msg" />
}

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

@if (messageImage.length) {
@if ((message | messageImageFile).length) {
<div class="chat-message__file">
@for (image of messageImage; track messageImage) {
@for (
image of message | messageImageFile;
track image.image_file.file_id
) {
<div>File ID: {{ image.image_file.file_id }}</div>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
justify-content: flex-end;

.chat-message {
border-bottom-left-radius: 0;
background: var(--color-primary-200);
border-bottom-right-radius: 0;
align-self: flex-end;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MarkdownModule } from 'ngx-markdown';

import { ChatMessageComponent } from './chat-message.component';
import { MarkdownModule } from 'ngx-markdown';

describe('ChatMessageComponent', () => {
let component: ChatMessageComponent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import { MarkdownComponent } from 'ngx-markdown';
import { ChatAudioComponent } from '../chat-audio/chat-audio.component';
import { NgClass } from '@angular/common';
import { ChatAvatarComponent } from '../chat-avatar/chat-avatar.component';
import {
getMessageImage,
getMessageText,
} from '../../controls/message-content/message-content.helpers';
import { ImageFileContentBlock } from 'openai/resources/beta/threads';
import { MessageImageFilePipe } from '../../../pipes/message-file.pipe';
import { AnnotationPipe } from '../../../pipes/annotation.pipe';
import { ChatAnnotationsComponent } from '../chat-annotations/chat-annotations.component';

@Component({
selector: 'ai-chat-message',
Expand All @@ -23,21 +21,16 @@ import { ImageFileContentBlock } from 'openai/resources/beta/threads';
MarkdownComponent,
ChatAudioComponent,
ChatAvatarComponent,
MessageImageFilePipe,
AnnotationPipe,
ChatAnnotationsComponent,
],
})
export class ChatMessageComponent {
@Input() message!: ChatMessage;
@Input() message!: Partial<ChatMessage>;
@Input() class = '';
chatRole = ChatRole;

get messageText(): string {
return getMessageText(this.message);
}

get messageImage(): ImageFileContentBlock[] {
return getMessageImage(this.message);
}

@HostBinding('class') get getClasses(): string {
return `${this.class} is-${this.message?.role || 'none'}`;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="messages">
@for (message of initialMessages.concat(messages); track message) {
@for (message of initialMessages.concat(messages); track message.id) {
<span #item>
<ai-chat-message [message]="message"></ai-chat-message>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import { ChatTipsComponent } from '../chat-tips/chat-tips.component';
],
})
export class ChatMessagesComponent implements AfterViewInit, OnChanges {
@Input() initialMessages: ChatMessage[] = [];
@Input() messages: ChatMessage[] = [];
@Input() initialMessages: Partial<ChatMessage>[] = [];
@Input() messages: Partial<ChatMessage>[] = [];
@Input() isTyping = false;
@Input() tips: string[] = [];
@Output() tipSelected$ = new EventEmitter<string>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@for (tip of tips; track tip) {
<ai-chat-tip (click)="tipSelected$.emit(tip)">
{{ tip }}
</ai-chat-tip>
@for (tip of tips; track $index) {
<ai-chat-tip (click)="tipSelected$.emit(tip)">
{{ tip }}
</ai-chat-tip>
}
5 changes: 4 additions & 1 deletion apps/spa/src/app/components/controls/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ export class FilesService {
files$ = new BehaviorSubject<File[]>([]);

add(files: FileList) {
const convertedFiles = Object.keys(files).map(
key => files[key as unknown as number],
);
const updatedFiles = [
...this.files$.value,
...Object.keys(files).map(key => files[key as unknown as number]),
...convertedFiles,
];

this.files$.next(updatedFiles);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, ElementRef, Input, ViewChild } from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { toSignal } from '@angular/core/rxjs-interop';
import { MessageContentService } from './message-content.service';
import { ControlItemComponent } from '../control-item/control-item.component';
import { ControlIconComponent } from '../control-icon/control-icon.component';
import { AiFilesDirective } from '../files/files.directive';
import { MessageContentService } from './message-content.service';

@Component({
selector: 'ai-message-content',
Expand Down
Loading

0 comments on commit c2f8eec

Please sign in to comment.