@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.
@@ -0,0 +1,313 @@
1
+ import { Injectable } from '@angular/core';
2
+ import { BehaviorSubject, Observable } from 'rxjs';
3
+
4
+ /**
5
+ * Types matching core SDK
6
+ */
7
+ export interface AiMessage {
8
+ id: string;
9
+ role: 'user' | 'assistant';
10
+ content: string;
11
+ timestamp: Date;
12
+ sources?: string[];
13
+ isStreaming?: boolean;
14
+ }
15
+
16
+ export interface AiChart {
17
+ type: 'line' | 'bar' | 'pie' | 'table';
18
+ title: string;
19
+ data: any;
20
+ config?: any;
21
+ }
22
+
23
+ export interface AiContext {
24
+ pageUrl: string;
25
+ pageTitle: string;
26
+ userId?: string;
27
+ sessionId?: string;
28
+ customData?: Record<string, any>;
29
+ }
30
+
31
+ export interface AiContextConfig {
32
+ enableContextTracking: boolean;
33
+ contextUpdateInterval?: number;
34
+ includePageMetadata?: boolean;
35
+ }
36
+
37
+ export interface AiQuery {
38
+ question: string;
39
+ context?: AiContext;
40
+ }
41
+
42
+ export interface AiResponse {
43
+ success: boolean;
44
+ answer: string;
45
+ sources?: string[];
46
+ charts?: AiChart[];
47
+ followUpQuestions?: string[];
48
+ error?: string;
49
+ }
50
+
51
+ export interface AiWidgetConfig {
52
+ apiEndpoint: string;
53
+ enableFloatingButton?: boolean;
54
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
55
+ theme?: 'light' | 'dark';
56
+ contextConfig?: AiContextConfig;
57
+ }
58
+
59
+ export interface AiAuthConfig {
60
+ jwtToken?: string;
61
+ apiKey?: string;
62
+ refreshTokenFn?: () => Promise<string>;
63
+ }
64
+
65
+ /**
66
+ * Angular Service for AI Widget SDK
67
+ * Provides reactive state management using RxJS
68
+ */
69
+ @Injectable({
70
+ providedIn: 'root'
71
+ })
72
+ export class AiWidgetService {
73
+ private config: AiWidgetConfig | null = null;
74
+ private authConfig: AiAuthConfig = {};
75
+
76
+ // State observables
77
+ private messagesSubject = new BehaviorSubject<AiMessage[]>([]);
78
+ private loadingSubject = new BehaviorSubject<boolean>(false);
79
+ private errorSubject = new BehaviorSubject<string | null>(null);
80
+ private contextSubject = new BehaviorSubject<AiContext | null>(null);
81
+ private chartsSubject = new BehaviorSubject<AiChart[]>([]);
82
+
83
+ // Public observables
84
+ messages$ = this.messagesSubject.asObservable();
85
+ loading$ = this.loadingSubject.asObservable();
86
+ error$ = this.errorSubject.asObservable();
87
+ context$ = this.contextSubject.asObservable();
88
+ charts$ = this.chartsSubject.asObservable();
89
+
90
+ constructor() {}
91
+
92
+ /**
93
+ * Initialize the AI Widget Service
94
+ */
95
+ initialize(config: AiWidgetConfig, authConfig?: AiAuthConfig): void {
96
+ this.config = config;
97
+ this.authConfig = authConfig || {};
98
+ this.syncContext();
99
+ }
100
+
101
+ /**
102
+ * Send a query to the AI backend
103
+ */
104
+ async sendQuery(question: string): Promise<AiResponse> {
105
+ if (!this.config) {
106
+ const error = 'AI Widget not initialized. Call initialize() first.';
107
+ this.errorSubject.next(error);
108
+ throw new Error(error);
109
+ }
110
+
111
+ this.loadingSubject.next(true);
112
+ this.errorSubject.next(null);
113
+
114
+ try {
115
+ // Add user message
116
+ const userMessage: AiMessage = {
117
+ id: `msg-${Date.now()}`,
118
+ role: 'user',
119
+ content: question,
120
+ timestamp: new Date()
121
+ };
122
+ this.messagesSubject.next([...this.messagesSubject.value, userMessage]);
123
+
124
+ // Call backend
125
+ const headers: Record<string, string> = {
126
+ 'Content-Type': 'application/json'
127
+ };
128
+
129
+ if (this.authConfig.jwtToken) {
130
+ headers['Authorization'] = `Bearer ${this.authConfig.jwtToken}`;
131
+ } else if (this.authConfig.apiKey) {
132
+ headers['X-API-Key'] = this.authConfig.apiKey;
133
+ }
134
+
135
+ const response = await fetch(`${this.config.apiEndpoint}/api/query`, {
136
+ method: 'POST',
137
+ headers,
138
+ body: JSON.stringify({
139
+ query: question,
140
+ context: this.contextSubject.value
141
+ })
142
+ });
143
+
144
+ if (!response.ok) {
145
+ throw new Error(`API error: ${response.status}`);
146
+ }
147
+
148
+ const data = await response.json() as AiResponse;
149
+
150
+ // Add assistant message
151
+ const assistantMessage: AiMessage = {
152
+ id: `msg-${Date.now()}`,
153
+ role: 'assistant',
154
+ content: data.answer,
155
+ timestamp: new Date(),
156
+ sources: data.sources,
157
+ isStreaming: false
158
+ };
159
+ this.messagesSubject.next([...this.messagesSubject.value, assistantMessage]);
160
+
161
+ // Update charts if any
162
+ if (data.charts && data.charts.length > 0) {
163
+ this.chartsSubject.next(data.charts);
164
+ }
165
+
166
+ this.loadingSubject.next(false);
167
+ return data;
168
+ } catch (err) {
169
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
170
+ this.errorSubject.next(errorMsg);
171
+ this.loadingSubject.next(false);
172
+ throw err;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Set JWT authentication token
178
+ */
179
+ setJwt(token: string): void {
180
+ this.authConfig.jwtToken = token;
181
+ }
182
+
183
+ /**
184
+ * Set API key authentication
185
+ */
186
+ setApiKey(key: string): void {
187
+ this.authConfig.apiKey = key;
188
+ }
189
+
190
+ /**
191
+ * Set custom context
192
+ */
193
+ setContext(context: AiContext): void {
194
+ this.contextSubject.next(context);
195
+ }
196
+
197
+ /**
198
+ * Get current context
199
+ */
200
+ getContext(): AiContext | null {
201
+ return this.contextSubject.value;
202
+ }
203
+
204
+ /**
205
+ * Clear all messages and reset state
206
+ */
207
+ clear(): void {
208
+ this.messagesSubject.next([]);
209
+ this.chartsSubject.next([]);
210
+ this.errorSubject.next(null);
211
+ }
212
+
213
+ /**
214
+ * Export chat history
215
+ */
216
+ exportChats(format: 'json' | 'csv' = 'json'): string {
217
+ const messages = this.messagesSubject.value;
218
+
219
+ if (format === 'json') {
220
+ return JSON.stringify(messages, null, 2);
221
+ }
222
+
223
+ // CSV format
224
+ const headers = ['Timestamp', 'Role', 'Content'];
225
+ const rows = messages.map(m => [
226
+ m.timestamp.toISOString(),
227
+ m.role,
228
+ `"${m.content.replace(/"/g, '""')}"`
229
+ ]);
230
+
231
+ const csvContent = [
232
+ headers.join(','),
233
+ ...rows.map(r => r.join(','))
234
+ ].join('\n');
235
+
236
+ return csvContent;
237
+ }
238
+
239
+ /**
240
+ * Check backend health
241
+ */
242
+ async checkHealth(): Promise<void> {
243
+ if (!this.config) {
244
+ throw new Error('AI Widget not initialized');
245
+ }
246
+
247
+ const response = await fetch(`${this.config.apiEndpoint}/health`);
248
+ if (!response.ok) {
249
+ throw new Error(`Health check failed: ${response.status}`);
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Sync page context automatically
255
+ */
256
+ private syncContext(): void {
257
+ if (!this.config?.contextConfig?.enableContextTracking) {
258
+ return;
259
+ }
260
+
261
+ const context: AiContext = {
262
+ pageUrl: window.location.href,
263
+ pageTitle: document.title,
264
+ sessionId: this.getOrCreateSessionId()
265
+ };
266
+
267
+ this.contextSubject.next(context);
268
+
269
+ // Update context on navigation
270
+ if (this.config.contextConfig?.contextUpdateInterval) {
271
+ setInterval(() => {
272
+ if (window.location.href !== context.pageUrl) {
273
+ context.pageUrl = window.location.href;
274
+ context.pageTitle = document.title;
275
+ this.contextSubject.next(context);
276
+ }
277
+ }, this.config.contextConfig.contextUpdateInterval);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Get or create session ID
283
+ */
284
+ private getOrCreateSessionId(): string {
285
+ let sessionId = sessionStorage.getItem('ai-widget-session-id');
286
+ if (!sessionId) {
287
+ sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
288
+ sessionStorage.setItem('ai-widget-session-id', sessionId);
289
+ }
290
+ return sessionId;
291
+ }
292
+
293
+ /**
294
+ * Get current messages (snapshot)
295
+ */
296
+ getMessages(): AiMessage[] {
297
+ return this.messagesSubject.value;
298
+ }
299
+
300
+ /**
301
+ * Get loading state (snapshot)
302
+ */
303
+ isLoading(): boolean {
304
+ return this.loadingSubject.value;
305
+ }
306
+
307
+ /**
308
+ * Get current error (snapshot)
309
+ */
310
+ getError(): string | null {
311
+ return this.errorSubject.value;
312
+ }
313
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @courseecho/ai-widget-angular
3
+ *
4
+ * AI Chat Widget for Angular 14+
5
+ *
6
+ * Public API exports
7
+ */
8
+
9
+ // Services
10
+ export { AiWidgetService } from './ai-widget.service';
11
+
12
+ // Components
13
+ export { AiChatWidgetComponent } from './ai-chat-widget.component';
14
+
15
+ // Module (for older Angular versions)
16
+ export { AiWidgetModule } from './ai-widget.module';
17
+
18
+ // Types
19
+ export type {
20
+ AiMessage,
21
+ AiChart,
22
+ AiContext,
23
+ AiContextConfig,
24
+ AiQuery,
25
+ AiResponse,
26
+ AiWidgetConfig,
27
+ AiAuthConfig
28
+ } from './ai-widget.service';
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "moduleResolution": "node",
17
+ "useDefineForClassFields": true,
18
+ "experimentalDecorators": true,
19
+ "emitDecoratorMetadata": true
20
+ },
21
+ "include": [
22
+ "src/**/*.ts"
23
+ ],
24
+ "exclude": [
25
+ "node_modules",
26
+ "dist",
27
+ "**/*.spec.ts"
28
+ ]
29
+ }