Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iris: Add chat bubble #9292

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
895 changes: 486 additions & 409 deletions package-lock.json

Large diffs are not rendered by default.

60 changes: 31 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
"node_modules"
],
"dependencies": {
"@angular/animations": "18.2.2",
"@angular/cdk": "18.2.2",
"@angular/common": "18.2.2",
"@angular/compiler": "18.2.2",
"@angular/core": "18.2.2",
"@angular/forms": "18.2.2",
"@angular/localize": "18.2.2",
"@angular/material": "18.2.2",
"@angular/platform-browser": "18.2.2",
"@angular/platform-browser-dynamic": "18.2.2",
"@angular/router": "18.2.2",
"@angular/service-worker": "18.2.2",
"@angular/animations": "18.2.3",
"@angular/cdk": "18.2.3",
"@angular/common": "18.2.3",
"@angular/compiler": "18.2.3",
"@angular/core": "18.2.3",
"@angular/forms": "18.2.3",
"@angular/localize": "18.2.3",
"@angular/material": "18.2.3",
"@angular/platform-browser": "18.2.3",
"@angular/platform-browser-dynamic": "18.2.3",
"@angular/router": "18.2.3",
"@angular/service-worker": "18.2.3",
"@ctrl/ngx-emoji-mart": "9.2.0",
"@danielmoncada/angular-datetime-picker": "18.1.0",
"@fingerprintjs/fingerprintjs": "4.4.3",
Expand All @@ -37,7 +37,7 @@
"@ng-bootstrap/ng-bootstrap": "17.0.1",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@sentry/angular": "8.27.0",
"@sentry/angular": "8.28.0",
"@swimlane/ngx-charts": "20.5.0",
"@swimlane/ngx-graph": "8.4.0",
"@vscode/codicons": "0.0.36",
Expand All @@ -46,9 +46,9 @@
"core-js": "3.38.1",
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
"diff-match-patch-typescript": "1.0.8",
"diff-match-patch-typescript": "1.1.0",
"dompurify": "3.1.6",
"export-to-csv": "1.3.0",
"export-to-csv": "1.4.0",
"fast-json-patch": "3.1.1",
"franc-min": "6.2.0",
"html-diff-ts": "1.4.2",
Expand All @@ -62,8 +62,8 @@
"ngx-infinite-scroll": "18.0.0",
"ngx-webstorage": "18.0.0",
"papaparse": "5.4.1",
"pdfjs-dist": "^4.5.136",
"posthog-js": "1.160.0",
"pdfjs-dist": "4.6.82",
"posthog-js": "1.160.3",
"rxjs": "7.8.1",
"showdown": "2.1.0",
"showdown-highlight": "3.1.0",
Expand Down Expand Up @@ -97,11 +97,12 @@
"eslint": "^9.9.0"
},
"eslint-plugin-jest": {
"@typescript-eslint/eslint-plugin": "^8.1.0"
"@typescript-eslint/eslint-plugin": "^8.4.0"
},
"jsdom": "24.1.1",
"katex": "0.16.11",
"postcss": "8.4.41",
"rimraf": "6.0.1",
"semver": "7.6.3",
"showdown-katex": {
"showdown": "2.1.0"
Expand All @@ -114,33 +115,33 @@
},
"devDependencies": {
"@angular-builders/jest": "18.0.0",
"@angular-devkit/build-angular": "18.2.2",
"@angular-devkit/build-angular": "18.2.3",
"@angular-eslint/builder": "18.3.0",
"@angular-eslint/eslint-plugin": "18.3.0",
"@angular-eslint/eslint-plugin-template": "18.3.0",
"@angular-eslint/schematics": "18.3.0",
"@angular-eslint/template-parser": "18.3.0",
"@angular/cli": "18.2.2",
"@angular/compiler-cli": "18.2.2",
"@angular/language-service": "18.2.2",
"@sentry/types": "8.27.0",
"@angular/cli": "18.2.3",
"@angular/compiler-cli": "18.2.3",
"@angular/language-service": "18.2.3",
"@sentry/types": "8.28.0",
"@types/crypto-js": "4.2.2",
"@types/d3-shape": "3.1.6",
"@types/dompurify": "3.0.5",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/node": "22.5.1",
"@types/node": "22.5.4",
"@types/papaparse": "5.3.14",
"@types/showdown": "2.0.6",
"@types/smoothscroll-polyfill": "0.3.4",
"@types/sockjs-client": "1.5.4",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "8.3.0",
"@typescript-eslint/parser": "8.3.0",
"@typescript-eslint/eslint-plugin": "8.4.0",
"@typescript-eslint/parser": "8.4.0",
"eslint": "9.9.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-deprecation": "3.0.0",
"eslint-plugin-jest": "28.8.1",
"eslint-plugin-jest": "28.8.3",
"eslint-plugin-jest-extended": "2.4.0",
"eslint-plugin-prettier": "5.2.1",
"folder-hash": "4.0.4",
Expand All @@ -152,10 +153,11 @@
"jest-fail-on-console": "3.3.0",
"jest-junit": "16.0.0",
"jest-preset-angular": "14.2.2",
"lint-staged": "15.2.9",
"lint-staged": "15.2.10",
"ng-mocks": "14.13.1",
"prettier": "3.3.3",
"sass": "1.77.8",
"rimraf": "6.0.1",
"sass": "1.78.0",
"ts-jest": "29.2.5",
"typescript": "5.5.4",
"weak-napi": "2.0.2"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
@if (!chatOpen) {
<div
#chatBubble
class="message-bubble"
(click)="handleButtonClick()"
[ngClass]="{ 'content-overflow': this.isOverflowing }"
[@expandAnimation]="newIrisMessage ? 'visible' : 'hidden'"
>
@if (newIrisMessage) {
<span [innerHTML]="newIrisMessage | htmlForMarkdown"></span>
@if (this.isOverflowing) {
<div class="read-more">
<span> See full message </span>
<fa-icon [icon]="faAngleDoubleDown" class="read-more-icon" />
</div>
}
}
</div>
<div class="chatbot-button">
<jhi-iris-logo [size]="IrisLogoSize.MEDIUM" [look]="IrisLogoLookDirection.LEFT" (click)="handleButtonClick()" />
@if (hasNewMessages) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,64 @@
}
}

.message-bubble {
--r: 13px; /* the radius */

position: absolute;
z-index: 50;
bottom: 100px;
right: 80px;
width: 350px;
height: 150px;
background-color: var(--iris-client-chat-background);
cursor: pointer;
transition: all 0.1s ease-in-out;
overflow-y: hidden;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
padding: 10px;
display: flex;
border-radius: var(--r);
border-bottom-right-radius: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);

&:hover {
transform: scale(1.05);
.read-more > span {

Check notice on line 38 in src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss#L38

Expected empty line before rule (rule-empty-line-before)
text-decoration: underline;
}
}
}
.content-overflow {

Check notice on line 43 in src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss#L43

Expected empty line before rule (rule-empty-line-before)
&::before {
content: '';
bottom: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background: linear-gradient(0, var(--iris-chat-bubble-fade) 2%, transparent);

Check warning on line 51 in src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/webapp/app/iris/exercise-chatbot/exercise-chatbot-button.component.scss#L51

Unexpected nonstandard direction (function-linear-gradient-no-nonstandard-direction)
}
}

.read-more {
position: absolute;
cursor: pointer;
bottom: 5px;
text-align: center;
padding: 0 10px;
width: 150px;
left: calc(50% - 75px);
font-size: 14px;
z-index: 100;
color: var(--link-color);
}

.hidden {
display: none;
}

.btn-circle {
width: 40px;
height: 40px;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,61 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Overlay } from '@angular/cdk/overlay';
import { ActivatedRoute } from '@angular/router';
import { IrisChatbotWidgetComponent } from 'app/iris/exercise-chatbot/widget/chatbot-widget.component';
import { Subscription } from 'rxjs';
import { faChevronDown, faCircle } from '@fortawesome/free-solid-svg-icons';
import { EMPTY, Subscription, filter, of, switchMap } from 'rxjs';
import { faAngleDoubleDown, faChevronDown, faCircle } from '@fortawesome/free-solid-svg-icons';
import { IrisLogoLookDirection, IrisLogoSize } from 'app/iris/iris-logo/iris-logo.component';
import { ChatServiceMode, IrisChatService } from 'app/iris/iris-chat.service';
import { animate, state, style, transition, trigger } from '@angular/animations';
import { IrisTextMessageContent } from 'app/entities/iris/iris-content-type.model';

@Component({
selector: 'jhi-exercise-chatbot-button',
templateUrl: './exercise-chatbot-button.component.html',
styleUrls: ['./exercise-chatbot-button.component.scss'],
animations: [
trigger('expandAnimation', [
state(
'hidden',
style({
opacity: 0,
transform: 'scale(0)',
transformOrigin: 'bottom right',
}),
),
state(
'visible',
style({
opacity: 1,
transform: 'scale(1)',
transformOrigin: 'bottom right',
}),
),
transition('hidden => visible', animate('300ms ease-out')),
transition('visible => hidden', animate('300ms ease-in')),
]),
],
})
export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
dialogRef: MatDialogRef<IrisChatbotWidgetComponent> | null = null;
chatOpen = false;
isOverflowing = false;
hasNewMessages = false;
newIrisMessage: string | undefined;

private readonly CHAT_BUBBLE_TIMEOUT = 10000;

private numNewMessagesSubscription: Subscription;
private paramsSubscription: Subscription;
private latestIrisMessageSubscription: Subscription;

// Icons
faCircle = faCircle;
faChevronDown = faChevronDown;
faAngleDoubleDown = faAngleDoubleDown;

@ViewChild('chatBubble') chatBubble: ElementRef;

protected readonly IrisLogoLookDirection = IrisLogoLookDirection;
protected readonly IrisLogoSize = IrisLogoSize;
Expand All @@ -35,7 +68,7 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
) {}

ngOnInit() {
// Subscribes to route params and gets the exerciseId from the route
// Subscribes to route params and gets the exerciseId from the router
this.paramsSubscription = this.route.params.subscribe((params) => {
const exerciseId = parseInt(params['exerciseId'], 10);
this.chatService.switchTo(ChatServiceMode.EXERCISE, exerciseId);
Expand All @@ -45,6 +78,24 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
this.numNewMessagesSubscription = this.chatService.numNewMessages.subscribe((num) => {
this.hasNewMessages = num > 0;
});
this.latestIrisMessageSubscription = this.chatService.newIrisMessage
.pipe(
filter((msg) => !!msg),
switchMap((msg) => {
if (msg!.content && msg!.content.length > 0) {
return of((msg!.content[0] as IrisTextMessageContent).textContent);
}
return EMPTY;
}),
)
.subscribe((message) => {
this.newIrisMessage = message;
setTimeout(() => this.checkOverflow(), 0);
setTimeout(() => {
this.newIrisMessage = undefined;
this.isOverflowing = false;
}, this.CHAT_BUBBLE_TIMEOUT);
});
}

ngOnDestroy() {
Expand All @@ -54,6 +105,9 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
}
this.numNewMessagesSubscription?.unsubscribe();
this.paramsSubscription.unsubscribe();
this.latestIrisMessageSubscription.unsubscribe();
this.newIrisMessage = undefined;
this.isOverflowing = false;
}

/**
Expand All @@ -71,18 +125,35 @@ export class IrisExerciseChatbotButtonComponent implements OnInit, OnDestroy {
}
}

/**
* Checks if the chat bubble is overflowing and sets isOverflowing to true if it is.
*/
public checkOverflow() {
const element = this.chatBubble.nativeElement;
this.isOverflowing = element.scrollHeight > element.clientHeight;
}

/**
* Opens the chat dialog using MatDialog.
* Sets the configuration options for the dialog, including position, size, and data.
*/
openChat() {
public openChat() {
this.chatOpen = true;
this.newIrisMessage = undefined;
this.isOverflowing = false;
this.dialogRef = this.dialog.open(IrisChatbotWidgetComponent, {
hasBackdrop: false,
scrollStrategy: this.overlay.scrollStrategies.noop(),
position: { bottom: '0px', right: '0px' },
disableClose: true,
});
this.dialogRef.afterClosed().subscribe(() => (this.chatOpen = false));
this.dialogRef.afterClosed().subscribe(() => this.handleDialogClose());
}

private handleDialogClose() {
this.chatOpen = false;
this.newIrisMessage = undefined;
}

protected readonly IrisTextMessageContent = IrisTextMessageContent;
}
Loading
Loading