@adzen/doohbot 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.editorconfig +17 -0
  2. package/.vscode/extensions.json +4 -0
  3. package/.vscode/launch.json +26 -0
  4. package/.vscode/settings.json +13 -0
  5. package/.vscode/tasks.json +42 -0
  6. package/README.md +58 -0
  7. package/adzen-doohbot-0.0.1.tgz +0 -0
  8. package/adzen-doohbot-1.0.0.tgz +0 -0
  9. package/angular.json +119 -0
  10. package/package.json +57 -0
  11. package/projects/doohbot/README.md +63 -0
  12. package/projects/doohbot/ng-package.json +16 -0
  13. package/projects/doohbot/package.json +12 -0
  14. package/projects/doohbot/src/lib/directives/draggable/draggable-dialog.directive.ts +62 -0
  15. package/projects/doohbot/src/lib/directives/draggable/draggable-dialog.module.ts +9 -0
  16. package/projects/doohbot/src/lib/directives/resizable/resizable-dialog.directive.ts +163 -0
  17. package/projects/doohbot/src/lib/directives/resizable/resizable-dialog.module.ts +9 -0
  18. package/projects/doohbot/src/lib/doohbot.html +216 -0
  19. package/projects/doohbot/src/lib/doohbot.scss +568 -0
  20. package/projects/doohbot/src/lib/doohbot.spec.ts +21 -0
  21. package/projects/doohbot/src/lib/doohbot.ts +345 -0
  22. package/projects/doohbot/src/lib/elements/elements.ts +25 -0
  23. package/projects/doohbot/src/lib/helpers/predefined_messages.ts +2 -0
  24. package/projects/doohbot/src/lib/inputs/doohbot-input.ts +25 -0
  25. package/projects/doohbot/src/lib/model/doohbot.intents.ts +24 -0
  26. package/projects/doohbot/src/lib/model/message.ts +13 -0
  27. package/projects/doohbot/src/lib/model/token.ts +3 -0
  28. package/projects/doohbot/src/lib/services/messaging.service.ts +76 -0
  29. package/projects/doohbot/src/lib/shared/chips/chips.html +9 -0
  30. package/projects/doohbot/src/lib/shared/chips/chips.scss +27 -0
  31. package/projects/doohbot/src/lib/shared/chips/chips.spec.ts +23 -0
  32. package/projects/doohbot/src/lib/shared/chips/chips.ts +18 -0
  33. package/projects/doohbot/src/lib/shared/snackbar/snackbar.html +7 -0
  34. package/projects/doohbot/src/lib/shared/snackbar/snackbar.scss +73 -0
  35. package/projects/doohbot/src/lib/shared/snackbar/snackbar.spec.ts +21 -0
  36. package/projects/doohbot/src/lib/shared/snackbar/snackbar.ts +44 -0
  37. package/projects/doohbot/src/lib/utils/material-override.scss +312 -0
  38. package/projects/doohbot/src/lib/utils/utility.scss +536 -0
  39. package/projects/doohbot/src/public-api.ts +5 -0
  40. package/projects/doohbot/tsconfig.lib.json +19 -0
  41. package/projects/doohbot/tsconfig.lib.prod.json +11 -0
  42. package/projects/doohbot/tsconfig.spec.json +14 -0
  43. package/projects/doohbot-element/public/favicon.ico +0 -0
  44. package/projects/doohbot-element/src/app/app.config.ts +12 -0
  45. package/projects/doohbot-element/src/app/app.html +1 -0
  46. package/projects/doohbot-element/src/app/app.routes.ts +3 -0
  47. package/projects/doohbot-element/src/app/app.scss +0 -0
  48. package/projects/doohbot-element/src/app/app.spec.ts +23 -0
  49. package/projects/doohbot-element/src/app/app.ts +10 -0
  50. package/projects/doohbot-element/src/index.html +15 -0
  51. package/projects/doohbot-element/src/main.ts +6 -0
  52. package/projects/doohbot-element/src/styles.scss +15 -0
  53. package/projects/doohbot-element/tsconfig.app.json +15 -0
  54. package/projects/doohbot-element/tsconfig.spec.json +14 -0
  55. package/tsconfig.json +43 -0
@@ -0,0 +1,345 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ effect,
6
+ ElementRef,
7
+ inject,
8
+ Input,
9
+ Renderer2,
10
+ signal,
11
+ TrackByFunction,
12
+ ViewChild,
13
+ ViewEncapsulation,
14
+ } from '@angular/core';
15
+ import { MessageService } from './services/messaging.service';
16
+ import { Message } from './model/message';
17
+ import { CommonModule } from '@angular/common';
18
+ import { OverlayContainer } from '@angular/cdk/overlay';
19
+ import { MatMenuModule } from '@angular/material/menu';
20
+ import { MatIcon } from '@angular/material/icon';
21
+ import { Observable } from 'rxjs';
22
+ import { DoohbotInput } from './inputs/doohbot-input';
23
+ import { SnackBar } from './shared/snackbar/snackbar';
24
+ import { MatBadgeModule } from '@angular/material/badge';
25
+ import { Chips } from './shared/chips/chips';
26
+ import { PredefinedMessages } from './helpers/predefined_messages';
27
+ import { MatTooltip } from '@angular/material/tooltip';
28
+ import { DraggableDialogDirective } from './directives/draggable/draggable-dialog.directive';
29
+ import { ResizableDialogDirective } from './directives/resizable/resizable-dialog.directive';
30
+
31
+ @Component({
32
+ selector: 'app-doohbot',
33
+ standalone: true,
34
+ imports: [
35
+ CommonModule,
36
+ MatIcon,
37
+ MatMenuModule,
38
+ SnackBar,
39
+ DraggableDialogDirective,
40
+ ResizableDialogDirective,
41
+ MatBadgeModule,
42
+ Chips,
43
+ MatTooltip,
44
+ ],
45
+ templateUrl: './doohbot.html',
46
+ styleUrl: './doohbot.scss',
47
+ changeDetection: ChangeDetectionStrategy.OnPush,
48
+ })
49
+ export class Doohbot extends DoohbotInput {
50
+ @Input() config: DoohbotInput = new DoohbotInput();
51
+ @Input() platformTenant!: string;
52
+ @Input() subTenant!: string;
53
+ @Input() agent!: string;
54
+ @Input() buttonStyle: 'fab' | 'sidebar' = 'fab';
55
+
56
+ isFullScreen = false;
57
+ showThemeMenu = false;
58
+ isMenuOpen: boolean = false;
59
+ currentUser: string = 'User';
60
+
61
+ public isChatOpen = signal(false);
62
+ public isBotTyping = signal(false);
63
+ private messageService = inject(MessageService);
64
+ public messages = this.messageService.messages;
65
+ private lastReadMessageId = signal<string | null>(null);
66
+ private mutationObserver!: MutationObserver;
67
+
68
+ maxMessageLength = 1000;
69
+ messageError = signal<string | null>(null);
70
+ @ViewChild('chatMessages') private chatMessagesContainer!: ElementRef;
71
+
72
+ $index: TrackByFunction<Message> | undefined;
73
+
74
+ theme: 'light' | 'dark' | 'system' = 'system';
75
+
76
+ settingsMenuItems: Array<{ label: string; action: () => void }> = [
77
+ { label: 'Light Theme', action: () => this.changeTheme('light') },
78
+ { label: 'Dark Theme', action: () => this.changeTheme('dark') },
79
+ { label: 'System Theme', action: () => this.changeTheme('system') },
80
+ ];
81
+
82
+ // Predefined messages for chips
83
+ public predefinedMessages: string[] = PredefinedMessages;
84
+
85
+ dismissTimeout: number | null = null;
86
+
87
+ ngOnInit() {
88
+ this.changeTheme(this.theme);
89
+ this.userAvatarUrl = this.config.userAvatarUrl || '';
90
+ }
91
+
92
+ getCurrentUser(): Observable<string> {
93
+ return new Observable((observer) => {
94
+ observer.next('');
95
+ });
96
+ }
97
+
98
+ // When user clicks a chip
99
+ onPredefinedClick(text: string) {
100
+ this.sendMessage(text);
101
+ }
102
+
103
+ changeTheme(mode: 'light' | 'dark' | 'system'): void {
104
+ this.theme = mode;
105
+ localStorage.setItem('theme', mode);
106
+ this.applyTheme(mode);
107
+ this.isMenuOpen = false;
108
+ }
109
+
110
+ private applyTheme(mode: 'light' | 'dark' | 'system') {
111
+ const body = document.body;
112
+ const host = this.elementRef.nativeElement;
113
+ const overlayContainer = this.overlay.getContainerElement();
114
+
115
+ // Remove existing theme classes
116
+ [body, host, overlayContainer].forEach((el) => {
117
+ this.renderer.removeClass(el, 'light-theme');
118
+ this.renderer.removeClass(el, 'dark-theme');
119
+ });
120
+
121
+ let themeToApply: 'light-theme' | 'dark-theme' = 'light-theme';
122
+
123
+ if (mode === 'system') {
124
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
125
+ themeToApply = prefersDark ? 'dark-theme' : 'light-theme';
126
+
127
+ // Live system change listener
128
+ window.matchMedia('(prefers-color-scheme: dark)').onchange = (e) => {
129
+ if (this.theme === 'system') {
130
+ const newTheme = e.matches ? 'dark-theme' : 'light-theme';
131
+ [body, host, overlayContainer].forEach((el) => {
132
+ this.renderer.removeClass(el, 'light-theme');
133
+ this.renderer.removeClass(el, 'dark-theme');
134
+ this.renderer.addClass(el, newTheme);
135
+ });
136
+ }
137
+ };
138
+ } else if (mode === 'dark') {
139
+ themeToApply = 'dark-theme';
140
+ }
141
+
142
+ // Apply the theme to all important elements
143
+ [body, host, overlayContainer].forEach((el) => this.renderer.addClass(el, themeToApply));
144
+ }
145
+ constructor(
146
+ private overlay: OverlayContainer,
147
+ private elementRef: ElementRef,
148
+ private renderer: Renderer2,
149
+ ) {
150
+ super();
151
+ effect(() => {
152
+ this.messages();
153
+ setTimeout(() => this.scrollToBottom(), 0);
154
+ });
155
+ }
156
+
157
+ openSettingsMenu(): void {
158
+ this.isMenuOpen = true;
159
+ }
160
+
161
+ public preFullscreenState: {
162
+ width: string;
163
+ height: string;
164
+ left: string;
165
+ top: string;
166
+ } | null = null;
167
+
168
+ toggleFullScreen() {
169
+ const chatWindow = document.querySelector('.chat-window') as HTMLElement;
170
+ if (!chatWindow) return;
171
+
172
+ if (!this.isFullScreen) {
173
+ // ENTER "FULLSCREEN" / Save current drag/resize state
174
+ this.preFullscreenState = {
175
+ width: chatWindow.style.width,
176
+ height: chatWindow.style.height,
177
+ left: chatWindow.style.left,
178
+ top: chatWindow.style.top,
179
+ };
180
+
181
+ // Clear inline styles that conflict with .fullscreen
182
+ chatWindow.style.width = '';
183
+ chatWindow.style.height = '';
184
+ chatWindow.style.left = '';
185
+ chatWindow.style.top = '';
186
+ chatWindow.style.transform = ''; // important!
187
+
188
+ // Activate centered "fullscreen"
189
+ chatWindow.classList.add('fullscreen');
190
+ this.isFullScreen = true;
191
+ } else {
192
+ // EXIT "FULLSCREEN"
193
+ chatWindow.classList.remove('fullscreen');
194
+ this.isFullScreen = false;
195
+
196
+ // Restore drag/resize state
197
+ if (this.preFullscreenState) {
198
+ chatWindow.style.width = this.preFullscreenState.width || '400px';
199
+ chatWindow.style.height = this.preFullscreenState.height || '500px';
200
+ chatWindow.style.left = this.preFullscreenState.left || 'auto';
201
+ chatWindow.style.top = this.preFullscreenState.top || 'auto';
202
+ chatWindow.style.transform = 'none';
203
+ }
204
+ }
205
+ }
206
+ public ngAfterViewInit(): void {
207
+ this.mutationObserver = new MutationObserver(() => {
208
+ this.scrollToBottom();
209
+ });
210
+
211
+ this.mutationObserver.observe(this.chatMessagesContainer.nativeElement, {
212
+ childList: true,
213
+ });
214
+ }
215
+
216
+ public scrollToBottom(): void {
217
+ try {
218
+ this.chatMessagesContainer.nativeElement.scrollTop =
219
+ this.chatMessagesContainer.nativeElement.scrollHeight;
220
+ } catch (err) {
221
+ console.error(err);
222
+ }
223
+ }
224
+
225
+ public ngOnDestroy(): void {
226
+ this.mutationObserver.disconnect();
227
+ }
228
+
229
+ toggleChat() {
230
+ const currentlyOpen = this.isChatOpen();
231
+ this.isChatOpen.update((open) => !open);
232
+
233
+ // When opening the chat, mark all messages as read
234
+ if (!currentlyOpen) {
235
+ const messages = this.messages();
236
+ if (messages.length > 0) {
237
+ // Mark the latest message as read
238
+ const lastMessage = messages[messages.length - 1];
239
+ this.lastReadMessageId.set(lastMessage.id ?? null);
240
+ }
241
+ }
242
+ }
243
+ clearChat() {
244
+ this.messageService.clearMessages();
245
+ }
246
+
247
+ public sendMessage(text: string): void {
248
+ const trimmedText = text.trim();
249
+ this.messageError();
250
+
251
+ if (!trimmedText) return;
252
+
253
+ if (trimmedText.length > this.maxMessageLength) {
254
+ this.messageError.set(this.errorMessage);
255
+
256
+ // Auto-dismiss
257
+ setTimeout(() => {
258
+ this.messageError();
259
+ }, 5000);
260
+ return;
261
+ }
262
+ this.messageService.addMessage({
263
+ sender: 'user',
264
+ text: trimmedText,
265
+ id: Date.now().toString(),
266
+ timestamp: new Date(),
267
+ });
268
+ this.isBotTyping.set(true);
269
+
270
+ setTimeout(() => {
271
+ const botReply = this.messageService.getBotReply(trimmedText);
272
+ const isFallback = botReply === this.messageService.getFallbackReply();
273
+ this.messageService.addMessage({
274
+ sender: 'bot',
275
+ text: botReply,
276
+ id: Date.now().toString(),
277
+ showSuggestions: isFallback,
278
+ });
279
+ this.isBotTyping.set(false);
280
+ }, 1000);
281
+ }
282
+
283
+ // Clears the message error.
284
+ public clearMessageError(): void {
285
+ this.messageError.set(null);
286
+
287
+ // Cancel pending auto-dismiss
288
+ if (this.dismissTimeout) {
289
+ clearTimeout(this.dismissTimeout);
290
+ this.dismissTimeout = null;
291
+ }
292
+ }
293
+
294
+ // Determines whether to show suggestion chips
295
+ showSuggestionChips = computed(() => {
296
+ const msgs = this.messages();
297
+ if (msgs.length === 0) return false;
298
+
299
+ // Find the last bot message
300
+ const lastBotMsg = [...msgs].reverse().find((m) => m.sender === 'bot');
301
+ return lastBotMsg?.showSuggestions === true;
302
+ });
303
+
304
+ // Mark last message as read
305
+ public markAsReadEffect = effect(() => {
306
+ const isOpen = this.isChatOpen();
307
+ const msgs = this.messages();
308
+
309
+ if (isOpen && msgs.length > 0) {
310
+ const lastMessage = msgs[msgs.length - 1];
311
+ if (lastMessage.id) {
312
+ // Only update if ID exists and it's newer than current
313
+ const currentLastRead = this.lastReadMessageId();
314
+ if (lastMessage.id !== currentLastRead) {
315
+ this.lastReadMessageId.set(lastMessage.id);
316
+ }
317
+ }
318
+ }
319
+ });
320
+
321
+ unreadCount = computed(() => {
322
+ // If chat is open, treat as all read
323
+ if (this.isChatOpen()) {
324
+ return 0;
325
+ }
326
+
327
+ const messages = this.messages();
328
+ if (messages.length === 0) return 0;
329
+
330
+ const lastReadId = this.lastReadMessageId();
331
+
332
+ // Find index of last read message
333
+ let lastReadIndex = -1;
334
+ if (lastReadId !== null) {
335
+ lastReadIndex = messages.findIndex((msg) => msg.id === lastReadId);
336
+ }
337
+
338
+ // Unread = bot messages after the last read message
339
+ const unreadBotMessages = messages
340
+ .slice(lastReadIndex + 1)
341
+ .filter((msg) => msg.sender === 'bot');
342
+
343
+ return unreadBotMessages.length;
344
+ });
345
+ }
@@ -0,0 +1,25 @@
1
+ import { NgModule, Injector } from '@angular/core';
2
+ import { BrowserModule } from '@angular/platform-browser';
3
+ import { createCustomElement } from '@angular/elements';
4
+ import { MatIconModule } from '@angular/material/icon';
5
+ import { platformBrowser } from '@angular/platform-browser';
6
+ import { Doohbot } from '../doohbot';
7
+
8
+ @NgModule({
9
+ imports: [BrowserModule, MatIconModule],
10
+ declarations: [],
11
+ })
12
+ export class ElementModule {
13
+ constructor(injector: Injector) {
14
+ const doohbotElement = createCustomElement(Doohbot, { injector });
15
+
16
+ // Check if already defined to avoid errors
17
+ if (!customElements.get('app-doohbot')) {
18
+ customElements.define('app-doohbot', doohbotElement);
19
+ }
20
+ }
21
+ }
22
+
23
+ platformBrowser()
24
+ .bootstrapModule(ElementModule)
25
+ .catch((err) => console.error(err));
@@ -0,0 +1,2 @@
1
+ // chat-config.ts
2
+ export const PredefinedMessages = ['Hello!', 'I need help', 'Contact support', 'News and Updates'];
@@ -0,0 +1,25 @@
1
+ export class DoohbotInput {
2
+ // inputs
3
+ userAvatarUrl: string = 'https://iili.io/KpEYoCP.png';
4
+
5
+ // Application Constants Text
6
+ readonly appTitle = 'DoohBot';
7
+ readonly appSubtitle = 'Welcome to DoohBot';
8
+ readonly welcomeDesc =
9
+ 'We provide a powerful live chat platform to help businesses connect with their customers, offer support, and increase sales through real-time conversations.';
10
+ readonly chatIcon = 'chat';
11
+ readonly sendIcon = 'send';
12
+ readonly closeIcon = 'close';
13
+ readonly clear = 'clear_all';
14
+ readonly minimizeIcon = 'remove';
15
+ readonly moreIcon = 'more_vert';
16
+ readonly fullscreenIcon = 'fullscreen';
17
+ readonly fullscreenExitIcon = 'fullscreen_exit';
18
+ readonly hintText = 'Please Enter Here';
19
+ errorMessage = 'The message you submitted was too long.';
20
+
21
+ // Application Constants Images
22
+ readonly appLogoUrl = 'https://iili.io/KmDhToN.md.png';
23
+ readonly botAvatarUrl = 'https://iili.io/KpER0Ge.png';
24
+ mode: string | undefined;
25
+ }
@@ -0,0 +1,24 @@
1
+ import { ChatIntent } from './message';
2
+
3
+ export const CHAT_INTENTS: ChatIntent[] = [
4
+ {
5
+ patterns: ['hello', 'hi', 'hey', 'greetings', 'good morning', 'good afternoon', 'good evening'],
6
+ response: '👋Hello! How can I help you today?',
7
+ },
8
+ {
9
+ patterns: ['how are you', 'how are you doing'],
10
+ response: "I'm just a bot, but I'm doing great! How about you?",
11
+ },
12
+ {
13
+ patterns: ['help', 'support', 'problem', 'about', 'issue', 'question', 'assist', 'Contact'],
14
+ response: 'Sure! Please tell me your issue, and I will try to assist you.',
15
+ },
16
+ {
17
+ patterns: ['bye', 'goodbye', 'see you', 'thanks', 'thank you', 'bye bye', 'see you later'],
18
+ response: 'Goodbye! Have a nice day!',
19
+ },
20
+ {
21
+ patterns: ['news and updates', 'updates', 'news', 'new'],
22
+ response: 'Stay tuned for the latest news and updates from our team!',
23
+ },
24
+ ];
@@ -0,0 +1,13 @@
1
+ export interface Message {
2
+ id: string;
3
+ sender: 'user' | 'bot';
4
+ senderName?: string;
5
+ text: string;
6
+ timestamp?: Date;
7
+ showSuggestions?: boolean;
8
+ }
9
+
10
+ export interface ChatIntent {
11
+ patterns: string[];
12
+ response: string;
13
+ }
@@ -0,0 +1,3 @@
1
+ import { InjectionToken } from '@angular/core';
2
+
3
+ export const DOOHBOT_API_URL = new InjectionToken<string>('DOOHBOT_API_URL');