@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/DEPRECATION_NOTICE.md +93 -0
- package/README.md +624 -0
- package/package.json +32 -0
- package/src/ai-chat-widget.component.ts +487 -0
- package/src/ai-widget.module.ts +41 -0
- package/src/ai-widget.service.ts +313 -0
- package/src/index.ts +28 -0
- package/tsconfig.json +29 -0
|
@@ -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
|
+
}
|