@courseecho/ai-widget-angular 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.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@courseecho/ai-widget-angular",
3
+ "version": "1.0.0",
4
+ "description": "AI Chat Widget for Angular - Standalone component and service",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "tsc --watch"
10
+ },
11
+ "peerDependencies": {
12
+ "@angular/common": "^19.0.0 || ^20.0.0 || ^21.0.0",
13
+ "@angular/core": "^19.0.0 || ^20.0.0 || ^21.0.0",
14
+ "@angular/forms": "^19.0.0 || ^20.0.0 || ^21.0.0",
15
+ "rxjs": "^7.8.0"
16
+ },
17
+ "keywords": [
18
+ "ai",
19
+ "chatbot",
20
+ "angular",
21
+ "widget",
22
+ "component",
23
+ "service",
24
+ "chat"
25
+ ],
26
+ "author": "CourseEcho",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/courseecho/ai-widget-sdk"
31
+ }
32
+ }
@@ -0,0 +1,487 @@
1
+ import { Component, OnInit, OnDestroy, Input } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { Subject } from 'rxjs';
5
+ import { takeUntil } from 'rxjs/operators';
6
+ import { AiWidgetService, AiMessage, AiWidgetConfig, AiAuthConfig } from './ai-widget.service';
7
+
8
+ /**
9
+ * AI Chat Widget Component for Angular
10
+ * Standalone component that can be used in any Angular application
11
+ */
12
+ @Component({
13
+ selector: 'app-ai-chat-widget',
14
+ standalone: true,
15
+ imports: [CommonModule, FormsModule],
16
+ template: `
17
+ <div class="ai-widget-container" [class]="'ai-widget-' + position">
18
+ <!-- Floating Button -->
19
+ <button
20
+ class="ai-widget-toggle"
21
+ (click)="toggleChat()"
22
+ [class.open]="isOpen"
23
+ title="AI Chat Widget">
24
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
25
+ <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>
26
+ </svg>
27
+ <span class="ai-widget-badge" *ngIf="(messages$ | async)?.length">{{ (messages$ | async)?.length }}</span>
28
+ </button>
29
+
30
+ <!-- Chat Window -->
31
+ <div class="ai-widget-chat-window" *ngIf="isOpen" [@slideIn]>
32
+ <!-- Header -->
33
+ <div class="ai-widget-header">
34
+ <h3>AI Assistant</h3>
35
+ <button class="ai-widget-close" (click)="toggleChat()">✕</button>
36
+ </div>
37
+
38
+ <!-- Messages -->
39
+ <div class="ai-widget-messages" #messagesContainer>
40
+ @for (msg of (messages$ | async); track msg.id) {
41
+ <div class="ai-widget-message"
42
+ [class.user]="msg.role === 'user'"
43
+ [class.assistant]="msg.role === 'assistant'">
44
+ <div class="ai-widget-message-content">
45
+ {{ msg.content }}
46
+ </div>
47
+ <div class="ai-widget-message-time">{{ msg.timestamp | date:'short' }}</div>
48
+ @if (msg.sources && msg.sources.length > 0) {
49
+ <div class="ai-widget-sources">
50
+ Sources: {{ msg.sources.join(', ') }}
51
+ </div>
52
+ }
53
+ </div>
54
+ }
55
+
56
+ <!-- Loading Indicator -->
57
+ @if (loading$ | async) {
58
+ <div class="ai-widget-typing">
59
+ <span></span><span></span><span></span>
60
+ </div>
61
+ }
62
+
63
+ <!-- Error Message -->
64
+ @if (error$ | async; as error) {
65
+ <div class="ai-widget-error">
66
+ ⚠️ {{ error }}
67
+ </div>
68
+ }
69
+ <!-- Input Area -->
70
+ <div class="ai-widget-input-area">
71
+ <input
72
+ type="text"
73
+ class="ai-widget-input"
74
+ [(ngModel)]="userInput"
75
+ (keyup.enter)="sendMessage()"
76
+ placeholder="Ask me anything..."
77
+ [disabled]="(loading$ | async)"
78
+ />
79
+ <button
80
+ class="ai-widget-send"
81
+ (click)="sendMessage()"
82
+ [disabled]="(loading$ | async)"
83
+ title="Send message">
84
+
85
+ </button>
86
+ </div>
87
+
88
+ <!-- Footer Actions -->
89
+ <div class="ai-widget-footer">
90
+ <button class="ai-widget-action" (click)="clearChat()" title="Clear chat">
91
+ Clear
92
+ </button>
93
+ <button class="ai-widget-action" (click)="exportChat()" title="Export chat">
94
+ Export
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ `,
100
+ hostDirectives: [],
101
+ styles: [`
102
+ .ai-widget-container {
103
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
104
+ --ai-primary: #3b82f6;
105
+ --ai-text: #1f2937;
106
+ --ai-bg: #ffffff;
107
+ --ai-border: #e5e7eb;
108
+ --ai-user-bg: #3b82f6;
109
+ --ai-assistant-bg: #f3f4f6;
110
+ }
111
+
112
+ .ai-widget-bottom-right {
113
+ position: fixed;
114
+ bottom: 20px;
115
+ right: 20px;
116
+ }
117
+
118
+ .ai-widget-bottom-left {
119
+ position: fixed;
120
+ bottom: 20px;
121
+ left: 20px;
122
+ }
123
+
124
+ .ai-widget-top-right {
125
+ position: fixed;
126
+ top: 20px;
127
+ right: 20px;
128
+ }
129
+
130
+ .ai-widget-top-left {
131
+ position: fixed;
132
+ top: 20px;
133
+ left: 20px;
134
+ }
135
+
136
+ .ai-widget-toggle {
137
+ width: 56px;
138
+ height: 56px;
139
+ border-radius: 28px;
140
+ background: var(--ai-primary);
141
+ color: white;
142
+ border: none;
143
+ cursor: pointer;
144
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ transition: all 0.3s ease;
149
+ position: relative;
150
+ }
151
+
152
+ .ai-widget-toggle:hover {
153
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
154
+ transform: scale(1.05);
155
+ }
156
+
157
+ .ai-widget-toggle.open {
158
+ background: #ef4444;
159
+ }
160
+
161
+ .ai-widget-badge {
162
+ position: absolute;
163
+ top: -8px;
164
+ right: -8px;
165
+ background: #ef4444;
166
+ color: white;
167
+ border-radius: 50%;
168
+ width: 24px;
169
+ height: 24px;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ font-size: 12px;
174
+ font-weight: bold;
175
+ }
176
+
177
+ .ai-widget-chat-window {
178
+ position: absolute;
179
+ bottom: 80px;
180
+ right: 0;
181
+ width: 400px;
182
+ height: 600px;
183
+ background: var(--ai-bg);
184
+ border: 1px solid var(--ai-border);
185
+ border-radius: 12px;
186
+ box-shadow: 0 5px 40px rgba(0, 0, 0, 0.16);
187
+ display: flex;
188
+ flex-direction: column;
189
+ animation: slideUp 0.3s ease;
190
+ z-index: 10000;
191
+ }
192
+
193
+ @keyframes slideUp {
194
+ from {
195
+ opacity: 0;
196
+ transform: translateY(20px);
197
+ }
198
+ to {
199
+ opacity: 1;
200
+ transform: translateY(0);
201
+ }
202
+ }
203
+
204
+ .ai-widget-header {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ align-items: center;
208
+ padding: 16px;
209
+ border-bottom: 1px solid var(--ai-border);
210
+ background: var(--ai-primary);
211
+ color: white;
212
+ }
213
+
214
+ .ai-widget-header h3 {
215
+ margin: 0;
216
+ font-size: 16px;
217
+ font-weight: 600;
218
+ }
219
+
220
+ .ai-widget-close {
221
+ background: none;
222
+ border: none;
223
+ color: white;
224
+ cursor: pointer;
225
+ font-size: 20px;
226
+ padding: 0;
227
+ width: 24px;
228
+ height: 24px;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ }
233
+
234
+ .ai-widget-messages {
235
+ flex: 1;
236
+ overflow-y: auto;
237
+ padding: 16px;
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 12px;
241
+ }
242
+
243
+ .ai-widget-message {
244
+ display: flex;
245
+ flex-direction: column;
246
+ align-items: flex-start;
247
+ gap: 4px;
248
+ }
249
+
250
+ .ai-widget-message.user {
251
+ align-items: flex-end;
252
+ }
253
+
254
+ .ai-widget-message-content {
255
+ padding: 12px 16px;
256
+ border-radius: 12px;
257
+ max-width: 80%;
258
+ line-height: 1.5;
259
+ font-size: 14px;
260
+ }
261
+
262
+ .ai-widget-message.user .ai-widget-message-content {
263
+ background: var(--ai-user-bg);
264
+ color: white;
265
+ }
266
+
267
+ .ai-widget-message.assistant .ai-widget-message-content {
268
+ background: var(--ai-assistant-bg);
269
+ color: var(--ai-text);
270
+ }
271
+
272
+ .ai-widget-message-time {
273
+ font-size: 12px;
274
+ color: #9ca3af;
275
+ padding: 0 12px;
276
+ }
277
+
278
+ .ai-widget-sources {
279
+ font-size: 12px;
280
+ color: #6366f1;
281
+ padding: 8px 12px;
282
+ background: #f0f4ff;
283
+ border-radius: 6px;
284
+ max-width: 80%;
285
+ }
286
+
287
+ .ai-widget-typing {
288
+ display: flex;
289
+ gap: 4px;
290
+ padding: 12px 16px;
291
+ }
292
+
293
+ .ai-widget-typing span {
294
+ width: 8px;
295
+ height: 8px;
296
+ border-radius: 50%;
297
+ background: var(--ai-primary);
298
+ animation: typing 1.4s infinite;
299
+ }
300
+
301
+ .ai-widget-typing span:nth-child(2) {
302
+ animation-delay: 0.2s;
303
+ }
304
+
305
+ .ai-widget-typing span:nth-child(3) {
306
+ animation-delay: 0.4s;
307
+ }
308
+
309
+ @keyframes typing {
310
+ 0%, 60%, 100% {
311
+ opacity: 0.3;
312
+ transform: translateY(0);
313
+ }
314
+ 30% {
315
+ opacity: 1;
316
+ transform: translateY(-10px);
317
+ }
318
+ }
319
+
320
+ .ai-widget-error {
321
+ background: #fee2e2;
322
+ color: #dc2626;
323
+ padding: 12px;
324
+ border-radius: 6px;
325
+ font-size: 14px;
326
+ margin: 8px 0;
327
+ }
328
+
329
+ .ai-widget-input-area {
330
+ display: flex;
331
+ gap: 8px;
332
+ padding: 12px;
333
+ border-top: 1px solid var(--ai-border);
334
+ background: var(--ai-bg);
335
+ }
336
+
337
+ .ai-widget-input {
338
+ flex: 1;
339
+ border: 1px solid var(--ai-border);
340
+ border-radius: 6px;
341
+ padding: 10px 12px;
342
+ font-size: 14px;
343
+ font-family: inherit;
344
+ }
345
+
346
+ .ai-widget-input:focus {
347
+ outline: none;
348
+ border-color: var(--ai-primary);
349
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
350
+ }
351
+
352
+ .ai-widget-send {
353
+ background: var(--ai-primary);
354
+ color: white;
355
+ border: none;
356
+ border-radius: 6px;
357
+ width: 36px;
358
+ height: 36px;
359
+ cursor: pointer;
360
+ font-size: 18px;
361
+ transition: background 0.2s;
362
+ }
363
+
364
+ .ai-widget-send:hover:not(:disabled) {
365
+ background: #2563eb;
366
+ }
367
+
368
+ .ai-widget-send:disabled {
369
+ opacity: 0.5;
370
+ cursor: not-allowed;
371
+ }
372
+
373
+ .ai-widget-footer {
374
+ display: flex;
375
+ gap: 8px;
376
+ padding: 8px 12px;
377
+ border-top: 1px solid var(--ai-border);
378
+ background: #fafbfc;
379
+ }
380
+
381
+ .ai-widget-action {
382
+ flex: 1;
383
+ background: var(--ai-border);
384
+ border: none;
385
+ border-radius: 4px;
386
+ padding: 8px 12px;
387
+ font-size: 12px;
388
+ cursor: pointer;
389
+ transition: background 0.2s;
390
+ color: var(--ai-text);
391
+ font-weight: 500;
392
+ }
393
+
394
+ .ai-widget-action:hover {
395
+ background: #d1d5db;
396
+ }
397
+
398
+ @media (max-width: 480px) {
399
+ .ai-widget-chat-window {
400
+ width: 100vw;
401
+ height: 100vh;
402
+ bottom: 0;
403
+ right: 0;
404
+ border-radius: 0;
405
+ }
406
+ }
407
+ `]
408
+ })
409
+ export class AiChatWidgetComponent implements OnInit, OnDestroy {
410
+ @Input() config!: AiWidgetConfig;
411
+ @Input() authConfig?: AiAuthConfig;
412
+
413
+ isOpen = false;
414
+ userInput = '';
415
+ position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' = 'bottom-right';
416
+
417
+ messages$ = this.aiService.messages$;
418
+ loading$ = this.aiService.loading$;
419
+ error$ = this.aiService.error$;
420
+ context$ = this.aiService.context$;
421
+ charts$ = this.aiService.charts$;
422
+
423
+ private destroy$ = new Subject<void>();
424
+
425
+ constructor(private aiService: AiWidgetService) {}
426
+
427
+ ngOnInit(): void {
428
+ if (!this.config) {
429
+ console.error('AiChatWidgetComponent: config input is required');
430
+ return;
431
+ }
432
+
433
+ this.position = this.config.position || 'bottom-right';
434
+ this.aiService.initialize(this.config, this.authConfig);
435
+
436
+ // Auto-scroll to latest message
437
+ this.messages$
438
+ .pipe(takeUntil(this.destroy$))
439
+ .subscribe(() => {
440
+ setTimeout(() => {
441
+ const container = document.querySelector('.ai-widget-messages');
442
+ if (container) {
443
+ container.scrollTop = container.scrollHeight;
444
+ }
445
+ }, 0);
446
+ });
447
+ }
448
+
449
+ ngOnDestroy(): void {
450
+ this.destroy$.next();
451
+ this.destroy$.complete();
452
+ }
453
+
454
+ toggleChat(): void {
455
+ this.isOpen = !this.isOpen;
456
+ }
457
+
458
+ async sendMessage(): Promise<void> {
459
+ if (!this.userInput.trim()) {
460
+ return;
461
+ }
462
+
463
+ const question = this.userInput;
464
+ this.userInput = '';
465
+
466
+ try {
467
+ await this.aiService.sendQuery(question);
468
+ } catch (error) {
469
+ console.error('Error sending message:', error);
470
+ }
471
+ }
472
+
473
+ clearChat(): void {
474
+ this.aiService.clear();
475
+ }
476
+
477
+ exportChat(): void {
478
+ const content = this.aiService.exportChats('json');
479
+ const blob = new Blob([content], { type: 'application/json' });
480
+ const url = URL.createObjectURL(blob);
481
+ const link = document.createElement('a');
482
+ link.href = url;
483
+ link.download = `chat-export-${Date.now()}.json`;
484
+ link.click();
485
+ URL.revokeObjectURL(url);
486
+ }
487
+ }
@@ -0,0 +1,41 @@
1
+ import { NgModule } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { AiWidgetService } from './ai-widget.service';
5
+ import { AiChatWidgetComponent } from './ai-chat-widget.component';
6
+
7
+ /**
8
+ * AI Widget Module for Angular
9
+ * Import this module to use the AI Chat Widget in your Angular app
10
+ *
11
+ * Usage:
12
+ * import { AiWidgetModule } from '@courseecho/ai-widget-angular';
13
+ *
14
+ * @NgModule({
15
+ * imports: [AiWidgetModule],
16
+ * ...
17
+ * })
18
+ * export class AppModule { }
19
+ */
20
+ @NgModule({
21
+ declarations: [AiChatWidgetComponent],
22
+ exports: [AiChatWidgetComponent],
23
+ imports: [CommonModule, FormsModule],
24
+ providers: [AiWidgetService]
25
+ })
26
+ export class AiWidgetModule { }
27
+
28
+ // Also export as standalone for Angular 14+ app
29
+ export { AiChatWidgetComponent } from './ai-chat-widget.component';
30
+ export { AiWidgetService } from './ai-widget.service';
31
+
32
+ export type {
33
+ AiMessage,
34
+ AiChart,
35
+ AiContext,
36
+ AiContextConfig,
37
+ AiQuery,
38
+ AiResponse,
39
+ AiWidgetConfig,
40
+ AiAuthConfig
41
+ } from './ai-widget.service';