@foisit/angular-wrapper 2.4.65 → 2.4.67

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.
@@ -1,906 +1,959 @@
1
- // dist/libs/angular-wrapper/fesm2022/foisit-angular-wrapper.mjs
2
- import * as i0 from "@angular/core";
3
- import { Component, Inject, Injectable, NgModule } from "@angular/core";
4
- import { CommonModule } from "@angular/common";
1
+ import * as i0 from '@angular/core';
2
+ import { Component, Inject, Injectable, NgModule } from '@angular/core';
3
+ import { CommonModule } from '@angular/common';
5
4
 
6
- // dist/libs/core/src/lib/command/command-handler.js
7
- import { __awaiter as __awaiter2 } from "tslib";
5
+ class AngularWrapperComponent {
6
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AngularWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.7", type: AngularWrapperComponent, isStandalone: true, selector: "lib-angular-wrapper", ngImport: i0, template: "<p>AngularWrapper works!</p>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
8
+ }
9
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AngularWrapperComponent, decorators: [{
10
+ type: Component,
11
+ args: [{ selector: 'lib-angular-wrapper', imports: [CommonModule], template: "<p>AngularWrapper works!</p>\n" }]
12
+ }] });
8
13
 
9
- // dist/libs/core/src/lib/ai/openai.service.js
10
- import { __awaiter } from "tslib";
11
- var OpenAIService = class {
12
- constructor(endpoint) {
13
- this.endpoint = endpoint || "https://foisit-ninja.netlify.app/.netlify/functions/intent";
14
- }
15
- determineIntent(userInput, availableCommands, context) {
16
- return __awaiter(this, void 0, void 0, function* () {
17
- try {
18
- const payload = {
19
- userInput,
20
- commands: availableCommands.map((cmd) => ({
21
- id: cmd.id,
22
- command: cmd.command,
23
- description: cmd.description,
24
- parameters: cmd.parameters
25
- // Send param schemas to AI
26
- })),
27
- context
28
- };
29
- const response = yield fetch(this.endpoint, {
30
- method: "POST",
31
- headers: {
32
- "Content-Type": "application/json"
33
- },
34
- body: JSON.stringify(payload)
35
- });
36
- if (!response.ok) {
37
- throw new Error(`Proxy API Error: ${response.statusText}`);
38
- }
39
- const result = yield response.json();
40
- return result;
41
- } catch (error) {
42
- console.error("OpenAIService Error:", error);
43
- return { type: "unknown" };
44
- }
45
- });
46
- }
47
- };
14
+ class OpenAIService {
15
+ endpoint;
16
+ constructor(endpoint) {
17
+ this.endpoint =
18
+ endpoint || 'https://foisit-ninja.netlify.app/.netlify/functions/intent';
19
+ }
20
+ async determineIntent(userInput, availableCommands, context) {
21
+ try {
22
+ // Prepare the payload
23
+ const payload = {
24
+ userInput,
25
+ commands: availableCommands.map((cmd) => ({
26
+ id: cmd.id,
27
+ command: cmd.command,
28
+ description: cmd.description,
29
+ parameters: cmd.parameters, // Send param schemas to AI
30
+ })),
31
+ context,
32
+ };
33
+ const response = await fetch(this.endpoint, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ },
38
+ body: JSON.stringify(payload),
39
+ });
40
+ if (!response.ok) {
41
+ throw new Error(`Proxy API Error: ${response.statusText}`);
42
+ }
43
+ const result = await response.json();
44
+ return result;
45
+ }
46
+ catch (error) {
47
+ console.error('OpenAIService Error:', error);
48
+ return { type: 'unknown' };
49
+ }
50
+ }
51
+ }
48
52
 
49
- // dist/libs/core/src/lib/command/command-handler.js
50
- var CommandHandler = class {
51
- constructor(arg = true) {
52
- var _a;
53
- this.commands = /* @__PURE__ */ new Map();
54
- this.openAIService = null;
55
- this.context = null;
56
- this.pendingConfirmation = null;
57
- this.enableSmartIntent = true;
58
- this.selectOptionsCache = /* @__PURE__ */ new Map();
59
- if (typeof arg === "boolean") {
60
- this.enableSmartIntent = arg;
61
- if (this.enableSmartIntent) {
62
- this.openAIService = new OpenAIService();
63
- }
64
- return;
65
- }
66
- this.enableSmartIntent = (_a = arg.enableSmartIntent) !== null && _a !== void 0 ? _a : true;
67
- if (this.enableSmartIntent) {
68
- this.openAIService = new OpenAIService(arg.intentEndpoint);
69
- }
70
- }
71
- /** Add a new command (string or object) */
72
- addCommand(commandOrObj, action) {
73
- let cmd;
74
- if (typeof commandOrObj === "string") {
75
- if (!action) {
76
- throw new Error("Action required when adding command by string.");
77
- }
78
- cmd = {
79
- id: commandOrObj.toLowerCase().replace(/\s+/g, "_"),
80
- command: commandOrObj.toLowerCase(),
81
- action
82
- };
83
- } else {
84
- cmd = Object.assign({}, commandOrObj);
85
- if (!cmd.id) {
86
- cmd.id = cmd.command.toLowerCase().replace(/\s+/g, "_");
87
- }
53
+ class CommandHandler {
54
+ commands = new Map();
55
+ openAIService = null;
56
+ context = null;
57
+ pendingConfirmation = null;
58
+ enableSmartIntent = true;
59
+ selectOptionsCache = new Map();
60
+ constructor(arg = true) {
61
+ if (typeof arg === 'boolean') {
62
+ this.enableSmartIntent = arg;
63
+ if (this.enableSmartIntent) {
64
+ this.openAIService = new OpenAIService();
65
+ }
66
+ return;
67
+ }
68
+ this.enableSmartIntent = arg.enableSmartIntent ?? true;
69
+ if (this.enableSmartIntent) {
70
+ this.openAIService = new OpenAIService(arg.intentEndpoint);
71
+ }
88
72
  }
89
- this.commands.set(cmd.command.toLowerCase(), cmd);
90
- if (cmd.id && cmd.id !== cmd.command) {
91
- }
92
- }
93
- /** Remove an existing command */
94
- removeCommand(command) {
95
- this.commands.delete(command.toLowerCase());
96
- }
97
- /** Execute a command by matching input */
98
- executeCommand(input) {
99
- return __awaiter2(this, void 0, void 0, function* () {
100
- var _a, _b, _c;
101
- if (typeof input === "object" && input !== null) {
102
- if (this.isStructured(input)) {
103
- const cmdId = String(input.commandId);
104
- const rawParams = (_a = input.params) !== null && _a !== void 0 ? _a : {};
105
- const cmd2 = this.getCommandById(cmdId);
106
- if (!cmd2)
107
- return { message: "That command is not available.", type: "error" };
108
- const params = this.sanitizeParamsForCommand(cmd2, rawParams);
109
- const requiredParams = ((_b = cmd2.parameters) !== null && _b !== void 0 ? _b : []).filter((p) => p.required);
110
- const missing = requiredParams.filter((p) => params[p.name] == null || params[p.name] === "");
111
- if (missing.length > 0) {
112
- this.context = { commandId: this.getCommandIdentifier(cmd2), params };
113
- return {
114
- message: `Please provide the required details for "${cmd2.command}".`,
115
- type: "form",
116
- fields: missing
73
+ /** Add a new command (string or object) */
74
+ addCommand(commandOrObj, action) {
75
+ let cmd;
76
+ if (typeof commandOrObj === 'string') {
77
+ if (!action) {
78
+ throw new Error('Action required when adding command by string.');
79
+ }
80
+ cmd = {
81
+ id: commandOrObj.toLowerCase().replace(/\s+/g, '_'),
82
+ command: commandOrObj.toLowerCase(),
83
+ action,
117
84
  };
118
- }
119
- if (cmd2.critical) {
120
- this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd2), params };
121
- return this.buildConfirmResponse(cmd2);
122
- }
123
- return this.safeRunAction(cmd2, params);
124
85
  }
125
- if (!this.context) {
126
- return { message: "Session expired or invalid context.", type: "error" };
86
+ else {
87
+ cmd = { ...commandOrObj };
88
+ if (!cmd.id) {
89
+ cmd.id = cmd.command.toLowerCase().replace(/\s+/g, '_');
90
+ }
127
91
  }
128
- const cmd = this.getCommandById(this.context.commandId);
129
- if (!cmd) {
130
- this.context = null;
131
- return { message: "Session expired or invalid context.", type: "error" };
92
+ this.commands.set(cmd.command.toLowerCase(), cmd);
93
+ // Also index by ID for AI matching
94
+ if (cmd.id && cmd.id !== cmd.command) {
95
+ // We store it by ID in a separate map or just rely on iteration for AI
96
+ // For simplicity, let's keep the main map keyed by trigger phrase,
97
+ // but we'll lookup by ID in AI flow.
132
98
  }
133
- if (Array.isArray(input)) {
134
- return { message: "Invalid form payload.", type: "error" };
99
+ }
100
+ /** Remove an existing command */
101
+ removeCommand(command) {
102
+ this.commands.delete(command.toLowerCase());
103
+ }
104
+ /** Execute a command by matching input */
105
+ async executeCommand(input) {
106
+ // 0) Handle object input
107
+ if (typeof input === 'object' && input !== null) {
108
+ // Direct command trigger from UI: { commandId, params? }
109
+ if (this.isStructured(input)) {
110
+ const cmdId = String(input.commandId);
111
+ const rawParams = input.params ?? {};
112
+ const cmd = this.getCommandById(cmdId);
113
+ if (!cmd)
114
+ return { message: 'That command is not available.', type: 'error' };
115
+ const params = this.sanitizeParamsForCommand(cmd, rawParams);
116
+ const requiredParams = (cmd.parameters ?? []).filter((p) => p.required);
117
+ const missing = requiredParams.filter((p) => params[p.name] == null || params[p.name] === '');
118
+ if (missing.length > 0) {
119
+ // Ask for missing params via form and store context
120
+ this.context = { commandId: this.getCommandIdentifier(cmd), params };
121
+ return {
122
+ message: `Please provide the required details for "${cmd.command}".`,
123
+ type: 'form',
124
+ fields: missing,
125
+ };
126
+ }
127
+ if (cmd.critical) {
128
+ this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd), params };
129
+ return this.buildConfirmResponse(cmd);
130
+ }
131
+ return this.safeRunAction(cmd, params);
132
+ }
133
+ // Otherwise treat as continuation of a previously requested form
134
+ if (!this.context) {
135
+ return { message: 'Session expired or invalid context.', type: 'error' };
136
+ }
137
+ const cmd = this.getCommandById(this.context.commandId);
138
+ if (!cmd) {
139
+ this.context = null;
140
+ return { message: 'Session expired or invalid context.', type: 'error' };
141
+ }
142
+ if (Array.isArray(input)) {
143
+ return { message: 'Invalid form payload.', type: 'error' };
144
+ }
145
+ const mergedParams = {
146
+ ...this.context.params,
147
+ ...input,
148
+ };
149
+ // If this was a critical command, ask for confirmation before executing.
150
+ if (cmd.critical) {
151
+ this.context = null;
152
+ this.pendingConfirmation = {
153
+ commandId: this.getCommandIdentifier(cmd),
154
+ params: mergedParams,
155
+ };
156
+ return this.buildConfirmResponse(cmd);
157
+ }
158
+ const result = await this.safeRunAction(cmd, mergedParams);
159
+ this.context = null;
160
+ return this.normalizeResponse(result);
135
161
  }
136
- const mergedParams = Object.assign(Object.assign({}, this.context.params), input);
137
- if (cmd.critical) {
138
- this.context = null;
139
- this.pendingConfirmation = {
140
- commandId: this.getCommandIdentifier(cmd),
141
- params: mergedParams
142
- };
143
- return this.buildConfirmResponse(cmd);
144
- }
145
- const result = yield this.safeRunAction(cmd, mergedParams);
146
- this.context = null;
147
- return this.normalizeResponse(result);
148
- }
149
- const trimmedInput = input.trim().toLowerCase();
150
- if (this.pendingConfirmation) {
151
- const normalized = trimmedInput;
152
- if (["yes", "y", "confirm", "ok", "okay"].includes(normalized)) {
153
- const { commandId, params } = this.pendingConfirmation;
154
- this.pendingConfirmation = null;
155
- const cmd = this.getCommandById(commandId);
156
- if (!cmd) {
157
- return { message: "That action is no longer available.", type: "error" };
158
- }
159
- return this.safeRunAction(cmd, params);
162
+ const trimmedInput = input.trim().toLowerCase();
163
+ // 1) Confirmation flow (Yes/No)
164
+ if (this.pendingConfirmation) {
165
+ const normalized = trimmedInput;
166
+ if (['yes', 'y', 'confirm', 'ok', 'okay'].includes(normalized)) {
167
+ const { commandId, params } = this.pendingConfirmation;
168
+ this.pendingConfirmation = null;
169
+ const cmd = this.getCommandById(commandId);
170
+ if (!cmd) {
171
+ return { message: 'That action is no longer available.', type: 'error' };
172
+ }
173
+ return this.safeRunAction(cmd, params);
174
+ }
175
+ if (['no', 'n', 'cancel', 'stop'].includes(normalized)) {
176
+ this.pendingConfirmation = null;
177
+ return { message: 'Cancelled.', type: 'success' };
178
+ }
179
+ return {
180
+ message: 'Please confirm: Yes or No.',
181
+ type: 'confirm',
182
+ options: [
183
+ { label: 'Yes', value: 'yes' },
184
+ { label: 'No', value: 'no' },
185
+ ],
186
+ };
160
187
  }
161
- if (["no", "n", "cancel", "stop"].includes(normalized)) {
162
- this.pendingConfirmation = null;
163
- return { message: "Cancelled.", type: "success" };
188
+ // 2) Exact match
189
+ const exactCommand = this.commands.get(trimmedInput);
190
+ if (exactCommand) {
191
+ const cmd = exactCommand;
192
+ // If the command needs params, collect them (no AI needed)
193
+ const requiredParams = (cmd.parameters ?? []).filter((p) => p.required);
194
+ if (requiredParams.length > 0) {
195
+ this.context = { commandId: this.getCommandIdentifier(cmd), params: {} };
196
+ return {
197
+ message: `Please provide the required details for "${cmd.command}".`,
198
+ type: 'form',
199
+ fields: requiredParams,
200
+ };
201
+ }
202
+ // Critical commands require confirmation
203
+ if (cmd.critical) {
204
+ this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd), params: {} };
205
+ return this.buildConfirmResponse(cmd);
206
+ }
207
+ // Always pass an object for params so actions can safely read optional fields.
208
+ return this.safeRunAction(cmd, {});
164
209
  }
165
- return {
166
- message: "Please confirm: Yes or No.",
167
- type: "confirm",
168
- options: [
169
- { label: "Yes", value: "yes" },
170
- { label: "No", value: "no" }
171
- ]
172
- };
173
- }
174
- const exactCommand = this.commands.get(trimmedInput);
175
- if (exactCommand) {
176
- const cmd = exactCommand;
177
- const requiredParams = ((_c = cmd.parameters) !== null && _c !== void 0 ? _c : []).filter((p) => p.required);
178
- if (requiredParams.length > 0) {
179
- this.context = { commandId: this.getCommandIdentifier(cmd), params: {} };
180
- return {
181
- message: `Please provide the required details for "${cmd.command}".`,
182
- type: "form",
183
- fields: requiredParams
184
- };
210
+ // 3) Deterministic (non-AI) fuzzy match using keywords/substring
211
+ const deterministic = await this.tryDeterministicMatch(trimmedInput);
212
+ if (deterministic)
213
+ return deterministic;
214
+ // 4) Smart intent fallback (AI)
215
+ if (this.enableSmartIntent && this.openAIService) {
216
+ const availableCommands = await this.getCommandsForAI();
217
+ const aiResult = await this.openAIService.determineIntent(trimmedInput, availableCommands, this.context);
218
+ return this.handleAIResult(aiResult);
185
219
  }
186
- if (cmd.critical) {
187
- this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd), params: {} };
188
- return this.buildConfirmResponse(cmd);
220
+ // No deterministic match available and AI is disabled (or no AI configured).
221
+ // Return an error so callers (wrappers) can trigger fallback handling.
222
+ if (!this.enableSmartIntent) {
223
+ return { message: "I'm not sure what you mean.", type: 'error' };
189
224
  }
190
- return this.safeRunAction(cmd, {});
191
- }
192
- const deterministic = yield this.tryDeterministicMatch(trimmedInput);
193
- if (deterministic)
194
- return deterministic;
195
- if (this.enableSmartIntent && this.openAIService) {
196
- const availableCommands = yield this.getCommandsForAI();
197
- const aiResult = yield this.openAIService.determineIntent(trimmedInput, availableCommands, this.context);
198
- return this.handleAIResult(aiResult);
199
- }
200
- if (!this.enableSmartIntent) {
201
- return { message: "I'm not sure what you mean.", type: "error" };
202
- }
203
- return this.listAllCommands();
204
- });
205
- }
206
- handleAIResult(result) {
207
- return __awaiter2(this, void 0, void 0, function* () {
208
- var _a, _b;
209
- if (result.type === "match" && result.match) {
210
- const cmd = this.getCommandById(result.match);
211
- if (!cmd) {
212
- return { message: "I'm not sure what you mean.", type: "error" };
213
- }
214
- const rawParams = (_a = result.params) !== null && _a !== void 0 ? _a : {};
215
- const sanitizedParams = this.sanitizeParamsForCommand(cmd, rawParams);
216
- const params = cmd.allowAiParamExtraction === false ? {} : sanitizedParams;
217
- const requiredParams = ((_b = cmd.parameters) !== null && _b !== void 0 ? _b : []).filter((p) => p.required);
218
- const missingRequired = requiredParams.filter((p) => params[p.name] == null || params[p.name] === "");
219
- if (result.incomplete || missingRequired.length > 0) {
220
- this.context = { commandId: this.getCommandIdentifier(cmd), params };
221
- const mustUseForm = cmd.collectRequiredViaForm !== false;
222
- const askSingle = !mustUseForm && this.shouldAskSingleQuestion(missingRequired);
223
- if (askSingle) {
224
- const names = missingRequired.map((p) => p.name).join(" and ");
225
+ // As a last resort, list available commands to let the user pick.
226
+ return this.listAllCommands();
227
+ }
228
+ async handleAIResult(result) {
229
+ if (result.type === 'match' && result.match) {
230
+ const cmd = this.getCommandById(result.match);
231
+ if (!cmd) {
232
+ return { message: "I'm not sure what you mean.", type: 'error' };
233
+ }
234
+ const rawParams = (result.params ?? {});
235
+ const sanitizedParams = this.sanitizeParamsForCommand(cmd, rawParams);
236
+ const params = cmd.allowAiParamExtraction === false ? {} : sanitizedParams;
237
+ const requiredParams = (cmd.parameters ?? []).filter((p) => p.required);
238
+ const missingRequired = requiredParams.filter((p) => params[p.name] == null || params[p.name] === '');
239
+ if (result.incomplete || missingRequired.length > 0) {
240
+ // Store partial params for continuation
241
+ this.context = { commandId: this.getCommandIdentifier(cmd), params };
242
+ const mustUseForm = cmd.collectRequiredViaForm !== false;
243
+ const askSingle = !mustUseForm && this.shouldAskSingleQuestion(missingRequired);
244
+ if (askSingle) {
245
+ const names = missingRequired.map((p) => p.name).join(' and ');
246
+ return {
247
+ message: result.message || `Please provide ${names}.`,
248
+ type: 'question',
249
+ };
250
+ }
251
+ return {
252
+ message: result.message || `Please fill in the missing details for "${cmd.command}".`,
253
+ type: 'form',
254
+ fields: missingRequired,
255
+ };
256
+ }
257
+ // Critical commands require confirmation
258
+ if (cmd.critical) {
259
+ this.pendingConfirmation = {
260
+ commandId: this.getCommandIdentifier(cmd),
261
+ params,
262
+ };
263
+ return this.buildConfirmResponse(cmd);
264
+ }
265
+ const actionResult = await cmd.action(params);
266
+ return this.normalizeResponse(actionResult);
267
+ }
268
+ if (result.type === 'ambiguous' && result.options && result.options.length) {
269
+ // Prefer clickable phrases that map to exact matches. Use commandId as value to
270
+ // make UI actions deterministic when the user clicks an option.
225
271
  return {
226
- message: result.message || `Please provide ${names}.`,
227
- type: "question"
272
+ message: result.message || 'Did you mean one of these?',
273
+ type: 'ambiguous',
274
+ options: result.options.map((o) => ({
275
+ label: o.label,
276
+ value: o.commandId ?? o.label,
277
+ commandId: o.commandId,
278
+ })),
228
279
  };
229
- }
230
- return {
231
- message: result.message || `Please fill in the missing details for "${cmd.command}".`,
232
- type: "form",
233
- fields: missingRequired
234
- };
235
280
  }
236
- if (cmd.critical) {
237
- this.pendingConfirmation = {
238
- commandId: this.getCommandIdentifier(cmd),
239
- params
240
- };
241
- return this.buildConfirmResponse(cmd);
242
- }
243
- const actionResult = yield cmd.action(params);
244
- return this.normalizeResponse(actionResult);
245
- }
246
- if (result.type === "ambiguous" && result.options && result.options.length) {
281
+ // Unknown / fallback: show all available commands
282
+ return this.listAllCommands();
283
+ }
284
+ sanitizeParamsForCommand(cmd, params) {
285
+ const sanitized = { ...(params ?? {}) };
286
+ for (const p of cmd.parameters ?? []) {
287
+ const value = sanitized[p.name];
288
+ if (p.type === 'string') {
289
+ if (typeof value !== 'string') {
290
+ delete sanitized[p.name];
291
+ continue;
292
+ }
293
+ const trimmed = value.trim();
294
+ if (!trimmed) {
295
+ delete sanitized[p.name];
296
+ continue;
297
+ }
298
+ sanitized[p.name] = trimmed;
299
+ }
300
+ if (p.type === 'number') {
301
+ const numeric = typeof value === 'number' ? value : Number(value?.toString().trim());
302
+ if (Number.isNaN(numeric)) {
303
+ delete sanitized[p.name];
304
+ continue;
305
+ }
306
+ if (typeof p.min === 'number' && numeric < p.min) {
307
+ delete sanitized[p.name];
308
+ continue;
309
+ }
310
+ if (typeof p.max === 'number' && numeric > p.max) {
311
+ delete sanitized[p.name];
312
+ continue;
313
+ }
314
+ sanitized[p.name] = numeric;
315
+ }
316
+ if (p.type === 'date') {
317
+ const v = sanitized[p.name];
318
+ if (typeof v === 'string') {
319
+ const s = v.trim();
320
+ if (!this.isIsoDateString(s)) {
321
+ delete sanitized[p.name];
322
+ }
323
+ else {
324
+ sanitized[p.name] = s;
325
+ }
326
+ }
327
+ else {
328
+ delete sanitized[p.name];
329
+ }
330
+ }
331
+ if (p.type === 'select') {
332
+ const v = typeof value === 'string' ? value : value?.toString();
333
+ if (!v) {
334
+ delete sanitized[p.name];
335
+ continue;
336
+ }
337
+ if (Array.isArray(p.options) && p.options.length > 0) {
338
+ const allowed = p.options.some((opt) => String(opt.value) === String(v));
339
+ if (!allowed) {
340
+ delete sanitized[p.name];
341
+ continue;
342
+ }
343
+ }
344
+ sanitized[p.name] = v;
345
+ }
346
+ // Defensive handling for file parameters: AI may return textual hints like "csv file".
347
+ // If we expect a File/Blob or a base64 data URL (when delivery='base64'), accept only
348
+ // those forms. Otherwise remove the param so the handler will ask the user via a form.
349
+ if (p.type === 'file') {
350
+ const val = sanitized[p.name];
351
+ const isFileLike = val && typeof val === 'object' && typeof val.name === 'string' && typeof val.size === 'number';
352
+ const isDataUrl = typeof val === 'string' && /^data:[^;]+;base64,/.test(val);
353
+ const delivery = p.delivery ?? 'file';
354
+ if (delivery === 'base64') {
355
+ // Accept either a data URL string or a File-like object
356
+ if (!isDataUrl && !isFileLike) {
357
+ delete sanitized[p.name];
358
+ }
359
+ }
360
+ else {
361
+ // delivery === 'file' -> expect a File/Blob object
362
+ if (!isFileLike) {
363
+ delete sanitized[p.name];
364
+ }
365
+ }
366
+ }
367
+ }
368
+ return sanitized;
369
+ }
370
+ isIsoDateString(value) {
371
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
372
+ return false;
373
+ const dt = new Date(`${value}T00:00:00Z`);
374
+ return !Number.isNaN(dt.getTime());
375
+ }
376
+ shouldAskSingleQuestion(missing) {
377
+ if (missing.length !== 1)
378
+ return false;
379
+ const t = missing[0].type;
380
+ return t === 'string' || t === 'number' || t === 'date';
381
+ }
382
+ buildConfirmResponse(cmd) {
247
383
  return {
248
- message: result.message || "Did you mean one of these?",
249
- type: "ambiguous",
250
- options: result.options.map((o) => {
251
- var _a2;
384
+ message: `Are you sure you want to run "${cmd.command}"?`,
385
+ type: 'confirm',
386
+ options: [
387
+ { label: 'Yes', value: 'yes' },
388
+ { label: 'No', value: 'no' },
389
+ ],
390
+ };
391
+ }
392
+ async tryDeterministicMatch(input) {
393
+ // Substring or keyword-based match before AI
394
+ const candidates = [];
395
+ for (const cmd of this.commands.values()) {
396
+ let score = 0;
397
+ const commandPhrase = cmd.command.toLowerCase();
398
+ if (input.includes(commandPhrase))
399
+ score += 5;
400
+ const keywords = cmd.keywords ?? [];
401
+ for (const kw of keywords) {
402
+ const k = kw.toLowerCase().trim();
403
+ if (!k)
404
+ continue;
405
+ if (input === k)
406
+ score += 4;
407
+ else if (input.includes(k))
408
+ score += 3;
409
+ }
410
+ if (score > 0)
411
+ candidates.push({ cmd, score });
412
+ }
413
+ if (candidates.length === 0)
414
+ return null;
415
+ candidates.sort((a, b) => b.score - a.score);
416
+ const topScore = candidates[0].score;
417
+ const top = candidates.filter((c) => c.score === topScore).slice(0, 3);
418
+ // If multiple equally-good matches, ask the user to choose.
419
+ if (top.length > 1) {
252
420
  return {
253
- label: o.label,
254
- value: (_a2 = o.commandId) !== null && _a2 !== void 0 ? _a2 : o.label,
255
- commandId: o.commandId
421
+ message: 'I think you mean one of these. Which one should I run?',
422
+ type: 'ambiguous',
423
+ options: top.map((c) => ({
424
+ label: c.cmd.command,
425
+ value: c.cmd.command,
426
+ commandId: c.cmd.id,
427
+ })),
256
428
  };
257
- })
258
- };
259
- }
260
- return this.listAllCommands();
261
- });
262
- }
263
- sanitizeParamsForCommand(cmd, params) {
264
- var _a, _b;
265
- const sanitized = Object.assign({}, params !== null && params !== void 0 ? params : {});
266
- for (const p of (_a = cmd.parameters) !== null && _a !== void 0 ? _a : []) {
267
- const value = sanitized[p.name];
268
- if (p.type === "string") {
269
- if (typeof value !== "string") {
270
- delete sanitized[p.name];
271
- continue;
272
- }
273
- const trimmed = value.trim();
274
- if (!trimmed) {
275
- delete sanitized[p.name];
276
- continue;
277
- }
278
- sanitized[p.name] = trimmed;
279
- }
280
- if (p.type === "number") {
281
- const numeric = typeof value === "number" ? value : Number(value === null || value === void 0 ? void 0 : value.toString().trim());
282
- if (Number.isNaN(numeric)) {
283
- delete sanitized[p.name];
284
- continue;
285
429
  }
286
- if (typeof p.min === "number" && numeric < p.min) {
287
- delete sanitized[p.name];
288
- continue;
430
+ const cmd = top[0].cmd;
431
+ // If params are required, ask with a form
432
+ const requiredParams = (cmd.parameters ?? []).filter((p) => p.required);
433
+ if (requiredParams.length > 0) {
434
+ this.context = { commandId: this.getCommandIdentifier(cmd), params: {} };
435
+ return {
436
+ message: `Please provide the required details for "${cmd.command}".`,
437
+ type: 'form',
438
+ fields: requiredParams,
439
+ };
289
440
  }
290
- if (typeof p.max === "number" && numeric > p.max) {
291
- delete sanitized[p.name];
292
- continue;
441
+ if (cmd.critical) {
442
+ this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd), params: {} };
443
+ return this.buildConfirmResponse(cmd);
293
444
  }
294
- sanitized[p.name] = numeric;
295
- }
296
- if (p.type === "date") {
297
- const v = sanitized[p.name];
298
- if (typeof v === "string") {
299
- const s = v.trim();
300
- if (!this.isIsoDateString(s)) {
301
- delete sanitized[p.name];
302
- } else {
303
- sanitized[p.name] = s;
304
- }
305
- } else {
306
- delete sanitized[p.name];
445
+ return this.safeRunAction(cmd, {});
446
+ }
447
+ async safeRunAction(cmd, params) {
448
+ try {
449
+ const result = await cmd.action(params ?? {});
450
+ return this.normalizeResponse(result);
307
451
  }
308
- }
309
- if (p.type === "select") {
310
- const v = typeof value === "string" ? value : value === null || value === void 0 ? void 0 : value.toString();
311
- if (!v) {
312
- delete sanitized[p.name];
313
- continue;
314
- }
315
- if (Array.isArray(p.options) && p.options.length > 0) {
316
- const allowed = p.options.some((opt) => String(opt.value) === String(v));
317
- if (!allowed) {
318
- delete sanitized[p.name];
319
- continue;
320
- }
452
+ catch {
453
+ return { message: 'Something went wrong while running that command.', type: 'error' };
321
454
  }
322
- sanitized[p.name] = v;
323
- }
324
- if (p.type === "file") {
325
- const val = sanitized[p.name];
326
- const isFileLike = val && typeof val === "object" && typeof val.name === "string" && typeof val.size === "number";
327
- const isDataUrl = typeof val === "string" && /^data:[^;]+;base64,/.test(val);
328
- const delivery = (_b = p.delivery) !== null && _b !== void 0 ? _b : "file";
329
- if (delivery === "base64") {
330
- if (!isDataUrl && !isFileLike) {
331
- delete sanitized[p.name];
332
- }
333
- } else {
334
- if (!isFileLike) {
335
- delete sanitized[p.name];
336
- }
455
+ }
456
+ async getCommandsForAI() {
457
+ const commands = Array.from(this.commands.values()).map((cmd) => ({
458
+ ...cmd,
459
+ parameters: cmd.parameters
460
+ ? cmd.parameters.map((param) => ({ ...param }))
461
+ : undefined,
462
+ }));
463
+ // Resolve async select options (cached) to give the model enough context.
464
+ await Promise.all(commands.map(async (cmd) => {
465
+ if (!cmd.parameters)
466
+ return;
467
+ await Promise.all(cmd.parameters.map(async (p) => {
468
+ if (p.type !== 'select' || !p.getOptions || (p.options && p.options.length))
469
+ return;
470
+ const cacheKey = `${cmd.id ?? cmd.command}:${p.name}`;
471
+ const cached = this.selectOptionsCache.get(cacheKey);
472
+ const now = Date.now();
473
+ if (cached && now - cached.ts < 60_000) {
474
+ p.options = cached.options;
475
+ return;
476
+ }
477
+ try {
478
+ const opts = await p.getOptions();
479
+ this.selectOptionsCache.set(cacheKey, { options: opts, ts: now });
480
+ p.options = opts;
481
+ }
482
+ catch {
483
+ // If options fail to load, keep as-is.
484
+ }
485
+ }));
486
+ }));
487
+ return commands;
488
+ }
489
+ getCommandById(id) {
490
+ for (const cmd of this.commands.values()) {
491
+ if (cmd.id === id)
492
+ return cmd;
337
493
  }
338
- }
494
+ return undefined;
339
495
  }
340
- return sanitized;
341
- }
342
- isIsoDateString(value) {
343
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
344
- return false;
345
- const dt = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
346
- return !Number.isNaN(dt.getTime());
347
- }
348
- shouldAskSingleQuestion(missing) {
349
- if (missing.length !== 1)
350
- return false;
351
- const t = missing[0].type;
352
- return t === "string" || t === "number" || t === "date";
353
- }
354
- buildConfirmResponse(cmd) {
355
- return {
356
- message: `Are you sure you want to run "${cmd.command}"?`,
357
- type: "confirm",
358
- options: [
359
- { label: "Yes", value: "yes" },
360
- { label: "No", value: "no" }
361
- ]
362
- };
363
- }
364
- tryDeterministicMatch(input) {
365
- return __awaiter2(this, void 0, void 0, function* () {
366
- var _a, _b;
367
- const candidates = [];
368
- for (const cmd2 of this.commands.values()) {
369
- let score = 0;
370
- const commandPhrase = cmd2.command.toLowerCase();
371
- if (input.includes(commandPhrase))
372
- score += 5;
373
- const keywords = (_a = cmd2.keywords) !== null && _a !== void 0 ? _a : [];
374
- for (const kw of keywords) {
375
- const k = kw.toLowerCase().trim();
376
- if (!k)
377
- continue;
378
- if (input === k)
379
- score += 4;
380
- else if (input.includes(k))
381
- score += 3;
382
- }
383
- if (score > 0)
384
- candidates.push({ cmd: cmd2, score });
385
- }
386
- if (candidates.length === 0)
387
- return null;
388
- candidates.sort((a, b) => b.score - a.score);
389
- const topScore = candidates[0].score;
390
- const top = candidates.filter((c) => c.score === topScore).slice(0, 3);
391
- if (top.length > 1) {
392
- return {
393
- message: "I think you mean one of these. Which one should I run?",
394
- type: "ambiguous",
395
- options: top.map((c) => ({
396
- label: c.cmd.command,
397
- value: c.cmd.command,
398
- commandId: c.cmd.id
399
- }))
400
- };
401
- }
402
- const cmd = top[0].cmd;
403
- const requiredParams = ((_b = cmd.parameters) !== null && _b !== void 0 ? _b : []).filter((p) => p.required);
404
- if (requiredParams.length > 0) {
405
- this.context = { commandId: this.getCommandIdentifier(cmd), params: {} };
496
+ listAllCommands() {
497
+ const options = Array.from(this.commands.values()).map((cmd) => ({
498
+ label: cmd.command,
499
+ value: cmd.id ?? cmd.command,
500
+ commandId: cmd.id ?? cmd.command,
501
+ }));
406
502
  return {
407
- message: `Please provide the required details for "${cmd.command}".`,
408
- type: "form",
409
- fields: requiredParams
503
+ message: 'Here are the available commands:',
504
+ type: 'ambiguous',
505
+ options,
410
506
  };
411
- }
412
- if (cmd.critical) {
413
- this.pendingConfirmation = { commandId: this.getCommandIdentifier(cmd), params: {} };
414
- return this.buildConfirmResponse(cmd);
415
- }
416
- return this.safeRunAction(cmd, {});
417
- });
418
- }
419
- safeRunAction(cmd, params) {
420
- return __awaiter2(this, void 0, void 0, function* () {
421
- try {
422
- const result = yield cmd.action(params !== null && params !== void 0 ? params : {});
423
- return this.normalizeResponse(result);
424
- } catch (_a) {
425
- return { message: "Something went wrong while running that command.", type: "error" };
426
- }
427
- });
428
- }
429
- getCommandsForAI() {
430
- return __awaiter2(this, void 0, void 0, function* () {
431
- const commands = Array.from(this.commands.values()).map((cmd) => Object.assign(Object.assign({}, cmd), { parameters: cmd.parameters ? cmd.parameters.map((param) => Object.assign({}, param)) : void 0 }));
432
- yield Promise.all(commands.map((cmd) => __awaiter2(this, void 0, void 0, function* () {
433
- if (!cmd.parameters)
434
- return;
435
- yield Promise.all(cmd.parameters.map((p) => __awaiter2(this, void 0, void 0, function* () {
436
- var _a;
437
- if (p.type !== "select" || !p.getOptions || p.options && p.options.length)
438
- return;
439
- const cacheKey = `${(_a = cmd.id) !== null && _a !== void 0 ? _a : cmd.command}:${p.name}`;
440
- const cached = this.selectOptionsCache.get(cacheKey);
441
- const now = Date.now();
442
- if (cached && now - cached.ts < 6e4) {
443
- p.options = cached.options;
507
+ }
508
+ normalizeResponse(result) {
509
+ if (typeof result === 'string') {
510
+ return { message: result, type: 'success' };
511
+ }
512
+ if (result && typeof result === 'object') {
513
+ return result;
514
+ }
515
+ return { message: 'Done', type: 'success' };
516
+ }
517
+ isStructured(input) {
518
+ return typeof input['commandId'] === 'string';
519
+ }
520
+ getCommandIdentifier(cmd) {
521
+ if (!cmd.id) {
522
+ cmd.id = cmd.command.toLowerCase().replace(/\s+/g, '_');
523
+ }
524
+ return cmd.id;
525
+ }
526
+ /** List all registered commands */
527
+ getCommands() {
528
+ return Array.from(this.commands.keys());
529
+ }
530
+ }
531
+
532
+ class TextToSpeech {
533
+ synth = (typeof window !== 'undefined' ? window.speechSynthesis : null);
534
+ speak(text, options) {
535
+ if (!this.synth) {
536
+ // eslint-disable-next-line no-console
537
+ console.error('SpeechSynthesis API is not supported in this environment.');
444
538
  return;
445
- }
446
- try {
447
- const opts = yield p.getOptions();
448
- this.selectOptionsCache.set(cacheKey, { options: opts, ts: now });
449
- p.options = opts;
450
- } catch (_b) {
451
- }
452
- })));
453
- })));
454
- return commands;
455
- });
456
- }
457
- getCommandById(id) {
458
- for (const cmd of this.commands.values()) {
459
- if (cmd.id === id)
460
- return cmd;
461
- }
462
- return void 0;
463
- }
464
- listAllCommands() {
465
- const options = Array.from(this.commands.values()).map((cmd) => {
466
- var _a, _b;
467
- return {
468
- label: cmd.command,
469
- value: (_a = cmd.id) !== null && _a !== void 0 ? _a : cmd.command,
470
- commandId: (_b = cmd.id) !== null && _b !== void 0 ? _b : cmd.command
471
- };
472
- });
473
- return {
474
- message: "Here are the available commands:",
475
- type: "ambiguous",
476
- options
477
- };
478
- }
479
- normalizeResponse(result) {
480
- if (typeof result === "string") {
481
- return { message: result, type: "success" };
482
- }
483
- if (result && typeof result === "object") {
484
- return result;
485
- }
486
- return { message: "Done", type: "success" };
487
- }
488
- isStructured(input) {
489
- return typeof input["commandId"] === "string";
490
- }
491
- getCommandIdentifier(cmd) {
492
- if (!cmd.id) {
493
- cmd.id = cmd.command.toLowerCase().replace(/\s+/g, "_");
494
- }
495
- return cmd.id;
496
- }
497
- /** List all registered commands */
498
- getCommands() {
499
- return Array.from(this.commands.keys());
500
- }
501
- };
539
+ }
540
+ const utterance = new SpeechSynthesisUtterance(text);
541
+ if (options) {
542
+ utterance.pitch = options.pitch || 1;
543
+ utterance.rate = options.rate || 1;
544
+ utterance.volume = options.volume || 1;
545
+ }
546
+ // Notify listeners (e.g., VoiceProcessor) to pause recognition while speaking
547
+ if (typeof window !== 'undefined') {
548
+ utterance.onstart = () => {
549
+ window.dispatchEvent(new CustomEvent('foisit:tts-start'));
550
+ };
551
+ utterance.onend = () => {
552
+ // eslint-disable-next-line no-console
553
+ console.log('Speech finished.');
554
+ window.dispatchEvent(new CustomEvent('foisit:tts-end'));
555
+ };
556
+ }
557
+ utterance.onerror = (event) => {
558
+ // eslint-disable-next-line no-console
559
+ console.error('Error during speech synthesis:', event.error);
560
+ };
561
+ this.synth.speak(utterance);
562
+ }
563
+ stopSpeaking() {
564
+ if (this.synth) {
565
+ this.synth.cancel();
566
+ }
567
+ }
568
+ }
502
569
 
503
- // dist/libs/core/src/lib/speech/text-to-speech.js
504
- var TextToSpeech = class {
505
- constructor() {
506
- this.synth = typeof window !== "undefined" ? window.speechSynthesis : null;
507
- }
508
- speak(text, options) {
509
- if (!this.synth) {
510
- console.error("SpeechSynthesis API is not supported in this environment.");
511
- return;
512
- }
513
- const utterance = new SpeechSynthesisUtterance(text);
514
- if (options) {
515
- utterance.pitch = options.pitch || 1;
516
- utterance.rate = options.rate || 1;
517
- utterance.volume = options.volume || 1;
518
- }
519
- if (typeof window !== "undefined") {
520
- utterance.onstart = () => {
521
- window.dispatchEvent(new CustomEvent("foisit:tts-start"));
522
- };
523
- utterance.onend = () => {
524
- console.log("Speech finished.");
525
- window.dispatchEvent(new CustomEvent("foisit:tts-end"));
526
- };
527
- }
528
- utterance.onerror = (event) => {
529
- console.error("Error during speech synthesis:", event.error);
530
- };
531
- this.synth.speak(utterance);
532
- }
533
- stopSpeaking() {
534
- if (this.synth) {
535
- this.synth.cancel();
570
+ class FallbackHandler {
571
+ fallbackMessage = 'Sorry, I didn’t understand that.';
572
+ setFallbackMessage(message) {
573
+ this.fallbackMessage = message;
536
574
  }
537
- }
538
- };
575
+ handleFallback(transcript) {
576
+ // eslint-disable-next-line no-console
577
+ if (transcript)
578
+ console.log(`Fallback triggered for: "${transcript}"`);
579
+ console.log(this.fallbackMessage);
580
+ new TextToSpeech().speak(this.fallbackMessage);
581
+ }
582
+ getFallbackMessage() {
583
+ return this.fallbackMessage;
584
+ }
585
+ }
539
586
 
540
- // dist/libs/core/src/lib/fallback/fallback-handler.js
541
- var FallbackHandler = class {
542
- constructor() {
543
- this.fallbackMessage = "Sorry, I didn\u2019t understand that.";
544
- }
545
- setFallbackMessage(message) {
546
- this.fallbackMessage = message;
547
- }
548
- handleFallback(transcript) {
549
- if (transcript)
550
- console.log(`Fallback triggered for: "${transcript}"`);
551
- console.log(this.fallbackMessage);
552
- new TextToSpeech().speak(this.fallbackMessage);
553
- }
554
- getFallbackMessage() {
555
- return this.fallbackMessage;
556
- }
557
- };
587
+ /* eslint-disable no-unused-vars */
558
588
 
559
- // dist/libs/core/src/lib/speech/voice-processor.js
560
- import { __awaiter as __awaiter3 } from "tslib";
561
- var getRecognitionCtor = () => {
562
- var _a, _b;
563
- if (typeof window === "undefined")
564
- return null;
565
- const w = window;
566
- return (_b = (_a = w.SpeechRecognition) !== null && _a !== void 0 ? _a : w.webkitSpeechRecognition) !== null && _b !== void 0 ? _b : null;
589
+ const getRecognitionCtor = () => {
590
+ if (typeof window === 'undefined')
591
+ return null;
592
+ const w = window;
593
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
567
594
  };
568
- var VoiceProcessor = class {
569
- // Debug logger helpers
570
- log(message) {
571
- if (this.debugEnabled && message) {
572
- console.log("[VoiceProcessor]", message);
573
- }
574
- }
575
- warn(message) {
576
- if (this.debugEnabled && message) {
577
- console.warn("[VoiceProcessor]", message);
578
- }
579
- }
580
- error(message) {
581
- if (this.debugEnabled && message) {
582
- console.error("[VoiceProcessor]", message);
583
- }
584
- }
585
- constructor(language = "en-US", options = {}) {
586
- var _a, _b;
587
- this.recognition = null;
588
- this.isListening = false;
589
- this.engineActive = false;
590
- this.intentionallyStopped = false;
591
- this.restartAllowed = true;
592
- this.lastStart = 0;
593
- this.backoffMs = 250;
594
- this.destroyed = false;
595
- this.resultCallback = null;
596
- this.ttsSpeaking = false;
597
- this.debugEnabled = true;
598
- this.restartTimer = null;
599
- this.prewarmed = false;
600
- this.hadResultThisSession = false;
601
- this.onTTSStart = () => {
602
- var _a2;
603
- this.ttsSpeaking = true;
604
- try {
605
- (_a2 = this.recognition) === null || _a2 === void 0 ? void 0 : _a2.stop();
606
- } catch (_b2) {
607
- }
608
- if (this.isListening) {
609
- this.emitStatus("speaking");
610
- }
611
- };
612
- this.onTTSEnd = () => {
613
- this.ttsSpeaking = false;
614
- if (this.isListening && this.restartAllowed) {
615
- this.safeRestart();
616
- } else {
617
- this.emitStatus(this.isListening ? "listening" : "idle");
618
- }
619
- };
620
- const Ctor = getRecognitionCtor();
621
- if (Ctor) {
622
- this.recognition = new Ctor();
623
- this.recognition.lang = language;
624
- this.recognition.interimResults = (_a = options.interimResults) !== null && _a !== void 0 ? _a : true;
625
- this.recognition.continuous = (_b = options.continuous) !== null && _b !== void 0 ? _b : true;
626
- this.recognition.onresult = (event) => this.handleResult(event, options);
627
- this.recognition.onend = () => this.handleEnd();
628
- this.recognition.onstart = () => {
629
- this.log("recognition onstart");
630
- this.engineActive = true;
631
- this.hadResultThisSession = false;
632
- if (this.restartTimer) {
633
- clearTimeout(this.restartTimer);
634
- this.restartTimer = null;
635
- }
636
- this.backoffMs = 250;
637
- if (this.isListening && !this.ttsSpeaking) {
638
- this.emitStatus("listening");
639
- }
640
- };
641
- const vrec = this.recognition;
642
- vrec.onaudiostart = () => this.log("onaudiostart");
643
- vrec.onsoundstart = () => this.log("onsoundstart");
644
- vrec.onspeechstart = () => this.log("onspeechstart");
645
- vrec.onspeechend = () => this.log("onspeechend");
646
- vrec.onsoundend = () => this.log("onsoundend");
647
- vrec.onaudioend = () => this.log("onaudioend");
648
- this.recognition.onerror = (event) => this.handleError(event);
649
- } else {
650
- this.recognition = null;
651
- this.emitStatus("unsupported");
652
- }
653
- if (typeof window !== "undefined") {
654
- window.addEventListener("foisit:tts-start", this.onTTSStart);
655
- window.addEventListener("foisit:tts-end", this.onTTSEnd);
656
- this.visibilityHandler = () => {
657
- var _a2;
658
- if (typeof document !== "undefined" && document.hidden) {
659
- try {
660
- (_a2 = this.recognition) === null || _a2 === void 0 ? void 0 : _a2.stop();
661
- } catch (_b2) {
662
- }
663
- this.emitStatus(this.ttsSpeaking ? "speaking" : "idle");
664
- } else if (this.isListening && !this.ttsSpeaking) {
665
- this.safeRestart();
595
+ class VoiceProcessor {
596
+ recognition = null;
597
+ isListening = false;
598
+ engineActive = false; // true after onstart, false after onend
599
+ intentionallyStopped = false;
600
+ restartAllowed = true;
601
+ lastStart = 0;
602
+ backoffMs = 250;
603
+ destroyed = false;
604
+ resultCallback = null;
605
+ ttsSpeaking = false;
606
+ visibilityHandler;
607
+ statusCallback;
608
+ debugEnabled = true; // enable debug logs to aid diagnosis
609
+ restartTimer = null;
610
+ prewarmed = false;
611
+ hadResultThisSession = false;
612
+ // Debug logger helpers
613
+ log(message) {
614
+ if (this.debugEnabled && message) {
615
+ // eslint-disable-next-line no-console
616
+ console.log('[VoiceProcessor]', message);
666
617
  }
667
- };
668
- if (typeof document !== "undefined") {
669
- document.addEventListener("visibilitychange", this.visibilityHandler);
670
- }
671
- } else {
672
- this.visibilityHandler = void 0;
673
- }
674
- }
675
- /** Check if SpeechRecognition is available */
676
- isSupported() {
677
- return getRecognitionCtor() !== null;
678
- }
679
- /** Allow consumers (wrappers) to observe status changes */
680
- onStatusChange(callback) {
681
- this.statusCallback = callback;
682
- }
683
- /** Start listening for speech input */
684
- startListening(callback) {
685
- if (!this.isSupported() || !this.recognition) {
686
- this.warn("VoiceProcessor: SpeechRecognition is not supported in this browser.");
687
- this.emitStatus("unsupported");
688
- return;
689
- }
690
- if (this.isListening) {
691
- this.warn("VoiceProcessor: Already listening.");
692
- this.resultCallback = callback;
693
- return;
694
- }
695
- this.resultCallback = callback;
696
- this.intentionallyStopped = false;
697
- this.restartAllowed = true;
698
- this.isListening = true;
699
- this.emitStatus("listening");
700
- this.prewarmAudio().finally(() => {
701
- this.safeRestart();
702
- });
703
- }
704
- /** Stop listening for speech input */
705
- stopListening() {
706
- var _a;
707
- this.intentionallyStopped = true;
708
- this.restartAllowed = false;
709
- this.isListening = false;
710
- this.emitStatus(this.ttsSpeaking ? "speaking" : "idle");
711
- try {
712
- (_a = this.recognition) === null || _a === void 0 ? void 0 : _a.stop();
713
- } catch (_b) {
714
- }
715
- }
716
- /** Clean up listeners */
717
- destroy() {
718
- this.destroyed = true;
719
- this.stopListening();
720
- this.resultCallback = null;
721
- window.removeEventListener("foisit:tts-start", this.onTTSStart);
722
- window.removeEventListener("foisit:tts-end", this.onTTSEnd);
723
- if (this.visibilityHandler) {
724
- document.removeEventListener("visibilitychange", this.visibilityHandler);
725
- this.visibilityHandler = void 0;
726
- }
727
- }
728
- /** Handle recognized speech results */
729
- handleResult(event, options) {
730
- var _a, _b, _c, _d;
731
- if (!this.resultCallback)
732
- return;
733
- const threshold = (_a = options.confidenceThreshold) !== null && _a !== void 0 ? _a : 0.6;
734
- for (let i = event.resultIndex; i < event.results.length; i++) {
735
- const res = event.results[i];
736
- const alt = res && res[0];
737
- const transcript = ((_c = (_b = alt === null || alt === void 0 ? void 0 : alt.transcript) === null || _b === void 0 ? void 0 : _b.trim) === null || _c === void 0 ? void 0 : _c.call(_b)) || "";
738
- const confidence = (_d = alt === null || alt === void 0 ? void 0 : alt.confidence) !== null && _d !== void 0 ? _d : 0;
739
- if (!transcript)
740
- continue;
741
- if (!res.isFinal && options.interimResults === false)
742
- continue;
743
- if (res.isFinal && confidence < threshold)
744
- continue;
745
- try {
746
- this.hadResultThisSession = true;
747
- this.resultCallback(transcript, !!res.isFinal);
748
- } catch (_e) {
749
- this.error("VoiceProcessor: result callback error");
750
- }
751
618
  }
752
- }
753
- /** Handle session end */
754
- handleEnd() {
755
- this.log("recognition onend");
756
- this.engineActive = false;
757
- if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking) {
758
- if (!this.ttsSpeaking) {
619
+ warn(message) {
620
+ if (this.debugEnabled && message) {
621
+ // eslint-disable-next-line no-console
622
+ console.warn('[VoiceProcessor]', message);
623
+ }
624
+ }
625
+ error(message) {
626
+ if (this.debugEnabled && message) {
627
+ // eslint-disable-next-line no-console
628
+ console.error('[VoiceProcessor]', message);
629
+ }
630
+ }
631
+ constructor(language = 'en-US', options = {}) {
632
+ const Ctor = getRecognitionCtor();
633
+ if (Ctor) {
634
+ this.recognition = new Ctor();
635
+ this.recognition.lang = language;
636
+ this.recognition.interimResults = options.interimResults ?? true;
637
+ this.recognition.continuous = options.continuous ?? true;
638
+ this.recognition.onresult = (event) => this.handleResult(event, options);
639
+ this.recognition.onend = () => this.handleEnd();
640
+ this.recognition.onstart = () => {
641
+ this.log('recognition onstart');
642
+ this.engineActive = true;
643
+ this.hadResultThisSession = false;
644
+ // Clear any pending restart attempts now that we are active
645
+ if (this.restartTimer) {
646
+ clearTimeout(this.restartTimer);
647
+ this.restartTimer = null;
648
+ }
649
+ this.backoffMs = 250;
650
+ if (this.isListening && !this.ttsSpeaking) {
651
+ this.emitStatus('listening');
652
+ }
653
+ };
654
+ const vrec = this.recognition;
655
+ vrec.onaudiostart = () => this.log('onaudiostart');
656
+ vrec.onsoundstart = () => this.log('onsoundstart');
657
+ vrec.onspeechstart = () => this.log('onspeechstart');
658
+ vrec.onspeechend = () => this.log('onspeechend');
659
+ vrec.onsoundend = () => this.log('onsoundend');
660
+ vrec.onaudioend = () => this.log('onaudioend');
661
+ this.recognition.onerror = (event) => this.handleError(event);
662
+ }
663
+ else {
664
+ // No native support; keep recognition null and let consumers feature-detect.
665
+ this.recognition = null;
666
+ // If unsupported, immediately report status so UI can show a clear fallback.
667
+ this.emitStatus('unsupported');
668
+ }
669
+ // Pause listening while TTS is speaking via CustomEvents dispatched by TextToSpeech
670
+ if (typeof window !== 'undefined') {
671
+ window.addEventListener('foisit:tts-start', this.onTTSStart);
672
+ window.addEventListener('foisit:tts-end', this.onTTSEnd);
673
+ // Pause on tab hide, resume on show
674
+ this.visibilityHandler = () => {
675
+ if (typeof document !== 'undefined' && document.hidden) {
676
+ try {
677
+ this.recognition?.stop();
678
+ }
679
+ catch { /* no-op */ }
680
+ this.emitStatus(this.ttsSpeaking ? 'speaking' : 'idle');
681
+ }
682
+ else if (this.isListening && !this.ttsSpeaking) {
683
+ this.safeRestart();
684
+ }
685
+ };
686
+ if (typeof document !== 'undefined') {
687
+ document.addEventListener('visibilitychange', this.visibilityHandler);
688
+ }
689
+ }
690
+ else {
691
+ // No window/document on server — do not register browser-only handlers
692
+ this.visibilityHandler = undefined;
693
+ }
694
+ }
695
+ /** Check if SpeechRecognition is available */
696
+ isSupported() {
697
+ return getRecognitionCtor() !== null;
698
+ }
699
+ /** Allow consumers (wrappers) to observe status changes */
700
+ onStatusChange(callback) {
701
+ this.statusCallback = callback;
702
+ }
703
+ /** Start listening for speech input */
704
+ startListening(callback) {
705
+ if (!this.isSupported() || !this.recognition) {
706
+ this.warn('VoiceProcessor: SpeechRecognition is not supported in this browser.');
707
+ this.emitStatus('unsupported');
708
+ return;
709
+ }
710
+ if (this.isListening) {
711
+ this.warn('VoiceProcessor: Already listening.');
712
+ this.resultCallback = callback; // update callback if needed
713
+ return;
714
+ }
715
+ this.resultCallback = callback;
716
+ this.intentionallyStopped = false;
717
+ this.restartAllowed = true;
718
+ this.isListening = true;
719
+ this.emitStatus('listening');
720
+ // Warm up mic to avoid immediate onend in some environments
721
+ this.prewarmAudio().finally(() => {
722
+ this.safeRestart();
723
+ });
724
+ }
725
+ /** Stop listening for speech input */
726
+ stopListening() {
727
+ this.intentionallyStopped = true;
728
+ this.restartAllowed = false;
759
729
  this.isListening = false;
760
- this.emitStatus("idle");
761
- }
762
- return;
763
- }
764
- this.isListening = true;
765
- this.scheduleRestart();
766
- }
767
- /** Handle errors during speech recognition */
768
- handleError(event) {
769
- const err = event === null || event === void 0 ? void 0 : event.error;
770
- this.warn(`Error occurred: ${err !== null && err !== void 0 ? err : "unknown"}`);
771
- const fatal = ["not-allowed", "service-not-allowed", "bad-grammar", "language-not-supported"];
772
- if (err && fatal.includes(err)) {
773
- this.intentionallyStopped = true;
774
- this.restartAllowed = false;
775
- this.isListening = false;
776
- this.emitStatus("error", { error: err });
777
- return;
778
- }
779
- this.scheduleRestart();
780
- }
781
- safeRestart() {
782
- if (!this.recognition)
783
- return;
784
- if (this.engineActive) {
785
- this.log("safeRestart: engine already active, skipping start");
786
- return;
787
- }
788
- const now = Date.now();
789
- if (now - this.lastStart < 300) {
790
- setTimeout(() => this.safeRestart(), 300);
791
- return;
792
- }
793
- this.lastStart = now;
794
- try {
795
- this.log("calling recognition.start()");
796
- this.recognition.start();
797
- this.backoffMs = 250;
798
- if (this.isListening && !this.ttsSpeaking) {
799
- this.emitStatus("listening");
800
- }
801
- } catch (_a) {
802
- this.error("recognition.start() threw; scheduling restart");
803
- this.scheduleRestart();
804
- }
805
- }
806
- scheduleRestart() {
807
- if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking)
808
- return;
809
- if (this.engineActive) {
810
- this.log("scheduleRestart: engine active, not scheduling");
811
- return;
812
- }
813
- const delay = Math.min(this.backoffMs, 2e3);
814
- this.log(`scheduleRestart in ${delay}ms`);
815
- if (this.restartTimer) {
816
- this.log("scheduleRestart: restart already scheduled");
817
- return;
818
- }
819
- this.restartTimer = setTimeout(() => {
820
- this.restartTimer = null;
821
- if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking)
822
- return;
823
- this.safeRestart();
824
- }, delay);
825
- this.backoffMs = Math.min(this.backoffMs * 2, 2e3);
826
- }
827
- prewarmAudio() {
828
- return __awaiter3(this, void 0, void 0, function* () {
829
- if (this.prewarmed)
830
- return;
831
- try {
832
- if (typeof navigator === "undefined" || !("mediaDevices" in navigator))
833
- return;
834
- const md = navigator.mediaDevices;
835
- if (!(md === null || md === void 0 ? void 0 : md.getUserMedia))
836
- return;
837
- this.log("prewarmAudio: requesting mic");
838
- const stream = yield md.getUserMedia({ audio: true });
839
- for (const track of stream.getTracks())
840
- track.stop();
841
- this.prewarmed = true;
842
- this.log("prewarmAudio: mic ready");
843
- } catch (_a) {
844
- this.warn("prewarmAudio: failed to get mic");
845
- }
846
- });
847
- }
848
- emitStatus(status, details) {
849
- if (!this.statusCallback)
850
- return;
851
- try {
852
- this.statusCallback(status, details);
853
- } catch (_a) {
854
- this.error("VoiceProcessor: status callback error");
855
- }
856
- }
857
- };
858
-
859
- // dist/libs/core/src/lib/utils/dom-interactions.js
860
- var GestureHandler = class {
861
- constructor() {
862
- this.lastTap = 0;
863
- }
864
- /**
865
- * Sets up double-click and double-tap listeners
866
- * @param onDoubleClickOrTap Callback to execute when a double-click or double-tap is detected
867
- */
868
- setupDoubleTapListener(onDoubleClickOrTap) {
869
- this.destroy();
870
- this.dblClickListener = () => {
871
- onDoubleClickOrTap();
730
+ this.emitStatus(this.ttsSpeaking ? 'speaking' : 'idle');
731
+ try {
732
+ this.recognition?.stop();
733
+ }
734
+ catch { /* no-op */ }
735
+ }
736
+ /** Clean up listeners */
737
+ destroy() {
738
+ this.destroyed = true;
739
+ this.stopListening();
740
+ this.resultCallback = null;
741
+ window.removeEventListener('foisit:tts-start', this.onTTSStart);
742
+ window.removeEventListener('foisit:tts-end', this.onTTSEnd);
743
+ if (this.visibilityHandler) {
744
+ document.removeEventListener('visibilitychange', this.visibilityHandler);
745
+ this.visibilityHandler = undefined;
746
+ }
747
+ }
748
+ /** Handle recognized speech results */
749
+ handleResult(event, options) {
750
+ if (!this.resultCallback)
751
+ return;
752
+ const threshold = options.confidenceThreshold ?? 0.6;
753
+ // Emit each alternative result chunk; concatenate finals client-side if desired
754
+ for (let i = event.resultIndex; i < event.results.length; i++) {
755
+ const res = event.results[i];
756
+ const alt = res && res[0];
757
+ const transcript = alt?.transcript?.trim?.() || '';
758
+ const confidence = alt?.confidence ?? 0;
759
+ if (!transcript)
760
+ continue;
761
+ if (!res.isFinal && options.interimResults === false)
762
+ continue; // skip interim if disabled
763
+ if (res.isFinal && confidence < threshold)
764
+ continue; // ignore low-confidence finals
765
+ try {
766
+ this.hadResultThisSession = true;
767
+ this.resultCallback(transcript, !!res.isFinal);
768
+ }
769
+ catch {
770
+ // Swallow user callback exceptions to avoid killing recognition
771
+ this.error('VoiceProcessor: result callback error');
772
+ }
773
+ }
774
+ }
775
+ /** Handle session end */
776
+ handleEnd() {
777
+ this.log('recognition onend');
778
+ this.engineActive = false;
779
+ if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking) {
780
+ if (!this.ttsSpeaking) {
781
+ this.isListening = false;
782
+ this.emitStatus('idle');
783
+ }
784
+ return;
785
+ }
786
+ // We are still in "listening" mode logically; recognition ended spuriously.
787
+ this.isListening = true;
788
+ // Best-effort restart (continuous can still end spuriously)
789
+ this.scheduleRestart();
790
+ }
791
+ /** Handle errors during speech recognition */
792
+ handleError(event) {
793
+ const err = event?.error;
794
+ this.warn(`Error occurred: ${err ?? 'unknown'}`);
795
+ // Fatal errors: don't spin
796
+ const fatal = ['not-allowed', 'service-not-allowed', 'bad-grammar', 'language-not-supported'];
797
+ if (err && fatal.includes(err)) {
798
+ this.intentionallyStopped = true;
799
+ this.restartAllowed = false;
800
+ this.isListening = false;
801
+ this.emitStatus('error', { error: err });
802
+ return;
803
+ }
804
+ // For transient errors, try restart
805
+ this.scheduleRestart();
806
+ }
807
+ safeRestart() {
808
+ if (!this.recognition)
809
+ return;
810
+ if (this.engineActive) {
811
+ this.log('safeRestart: engine already active, skipping start');
812
+ return;
813
+ }
814
+ const now = Date.now();
815
+ if (now - this.lastStart < 300) {
816
+ setTimeout(() => this.safeRestart(), 300);
817
+ return;
818
+ }
819
+ this.lastStart = now;
820
+ try {
821
+ this.log('calling recognition.start()');
822
+ this.recognition.start();
823
+ this.backoffMs = 250; // reset backoff on successful start
824
+ if (this.isListening && !this.ttsSpeaking) {
825
+ this.emitStatus('listening');
826
+ }
827
+ }
828
+ catch {
829
+ this.error('recognition.start() threw; scheduling restart');
830
+ this.scheduleRestart();
831
+ }
832
+ }
833
+ scheduleRestart() {
834
+ if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking)
835
+ return;
836
+ if (this.engineActive) {
837
+ this.log('scheduleRestart: engine active, not scheduling');
838
+ return;
839
+ }
840
+ const delay = Math.min(this.backoffMs, 2000);
841
+ this.log(`scheduleRestart in ${delay}ms`);
842
+ if (this.restartTimer) {
843
+ // A restart is already scheduled; keep the earliest
844
+ this.log('scheduleRestart: restart already scheduled');
845
+ return;
846
+ }
847
+ this.restartTimer = setTimeout(() => {
848
+ this.restartTimer = null;
849
+ if (this.destroyed || this.intentionallyStopped || !this.restartAllowed || this.ttsSpeaking)
850
+ return;
851
+ this.safeRestart();
852
+ }, delay);
853
+ this.backoffMs = Math.min(this.backoffMs * 2, 2000);
854
+ }
855
+ async prewarmAudio() {
856
+ if (this.prewarmed)
857
+ return;
858
+ try {
859
+ if (typeof navigator === 'undefined' || !('mediaDevices' in navigator))
860
+ return;
861
+ const md = navigator.mediaDevices;
862
+ if (!md?.getUserMedia)
863
+ return;
864
+ this.log('prewarmAudio: requesting mic');
865
+ const stream = await md.getUserMedia({ audio: true });
866
+ for (const track of stream.getTracks())
867
+ track.stop();
868
+ this.prewarmed = true;
869
+ this.log('prewarmAudio: mic ready');
870
+ }
871
+ catch {
872
+ this.warn('prewarmAudio: failed to get mic');
873
+ }
874
+ }
875
+ onTTSStart = () => {
876
+ this.ttsSpeaking = true;
877
+ try {
878
+ this.recognition?.stop();
879
+ }
880
+ catch { /* no-op */ }
881
+ // If we were listening, switch to speaking state
882
+ if (this.isListening) {
883
+ this.emitStatus('speaking');
884
+ }
872
885
  };
873
- document.addEventListener("dblclick", this.dblClickListener);
874
- this.touchEndListener = () => {
875
- const currentTime = (/* @__PURE__ */ new Date()).getTime();
876
- const tapInterval = currentTime - this.lastTap;
877
- if (tapInterval < 300 && tapInterval > 0) {
878
- onDoubleClickOrTap();
879
- }
880
- this.lastTap = currentTime;
886
+ onTTSEnd = () => {
887
+ this.ttsSpeaking = false;
888
+ if (this.isListening && this.restartAllowed) {
889
+ this.safeRestart();
890
+ }
891
+ else {
892
+ this.emitStatus(this.isListening ? 'listening' : 'idle');
893
+ }
881
894
  };
882
- document.addEventListener("touchend", this.touchEndListener);
883
- }
884
- destroy() {
885
- if (this.dblClickListener) {
886
- document.removeEventListener("dblclick", this.dblClickListener);
887
- }
888
- if (this.touchEndListener) {
889
- document.removeEventListener("touchend", this.touchEndListener);
890
- }
891
- this.dblClickListener = void 0;
892
- this.touchEndListener = void 0;
893
- }
894
- };
895
+ emitStatus(status, details) {
896
+ if (!this.statusCallback)
897
+ return;
898
+ try {
899
+ this.statusCallback(status, details);
900
+ }
901
+ catch {
902
+ // Never let consumer errors break recognition
903
+ this.error('VoiceProcessor: status callback error');
904
+ }
905
+ }
906
+ }
907
+
908
+ class GestureHandler {
909
+ lastTap = 0;
910
+ dblClickListener;
911
+ touchEndListener;
912
+ /**
913
+ * Sets up double-click and double-tap listeners
914
+ * @param onDoubleClickOrTap Callback to execute when a double-click or double-tap is detected
915
+ */
916
+ setupDoubleTapListener(onDoubleClickOrTap) {
917
+ // Ensure we never stack multiple listeners for the same instance
918
+ this.destroy();
919
+ // Handle double-click (desktop)
920
+ this.dblClickListener = () => {
921
+ onDoubleClickOrTap();
922
+ };
923
+ document.addEventListener('dblclick', this.dblClickListener);
924
+ // Handle double-tap (mobile)
925
+ this.touchEndListener = () => {
926
+ const currentTime = new Date().getTime();
927
+ const tapInterval = currentTime - this.lastTap;
928
+ if (tapInterval < 300 && tapInterval > 0) {
929
+ onDoubleClickOrTap();
930
+ }
931
+ this.lastTap = currentTime;
932
+ };
933
+ document.addEventListener('touchend', this.touchEndListener);
934
+ }
935
+ destroy() {
936
+ if (this.dblClickListener) {
937
+ document.removeEventListener('dblclick', this.dblClickListener);
938
+ }
939
+ if (this.touchEndListener) {
940
+ document.removeEventListener('touchend', this.touchEndListener);
941
+ }
942
+ this.dblClickListener = undefined;
943
+ this.touchEndListener = undefined;
944
+ }
945
+ }
895
946
  function injectStyles() {
896
- const existingStyle = document.querySelector("#assistant-styles");
897
- if (existingStyle) {
898
- console.log("Styles already injected");
899
- return;
900
- }
901
- const style = document.createElement("style");
902
- style.id = "assistant-styles";
903
- style.innerHTML = `
947
+ // Check if the styles are already injected
948
+ const existingStyle = document.querySelector('#assistant-styles');
949
+ if (existingStyle) {
950
+ console.log('Styles already injected');
951
+ return; // Avoid duplicate injection
952
+ }
953
+ // Create and inject the style element
954
+ const style = document.createElement('style');
955
+ style.id = 'assistant-styles';
956
+ style.innerHTML = `
904
957
  /* Rounded shape with gradient animation */
905
958
  .gradient-indicator {
906
959
  position: fixed;
@@ -934,644 +987,677 @@ function injectStyles() {
934
987
  }
935
988
  }
936
989
  `;
937
- document.head.appendChild(style);
938
- console.log("Gradient styles injected");
990
+ document.head.appendChild(style);
991
+ console.log('Gradient styles injected');
939
992
  }
940
993
  function addGradientAnimation() {
941
- if (document.querySelector("#gradient-indicator")) {
942
- return;
943
- }
944
- const gradientDiv = document.createElement("div");
945
- gradientDiv.id = "gradient-indicator";
946
- injectStyles();
947
- gradientDiv.classList.add("gradient-indicator");
948
- document.body.appendChild(gradientDiv);
949
- console.log("Gradient indicator added to the DOM");
994
+ // Check if the gradient indicator already exists
995
+ if (document.querySelector('#gradient-indicator')) {
996
+ return; // Avoid duplicate indicators
997
+ }
998
+ // Create a new div element
999
+ const gradientDiv = document.createElement('div');
1000
+ gradientDiv.id = 'gradient-indicator';
1001
+ // Inject styles dynamically
1002
+ injectStyles();
1003
+ // Add the gradient-indicator class to the div
1004
+ gradientDiv.classList.add('gradient-indicator');
1005
+ // Append the div to the body
1006
+ document.body.appendChild(gradientDiv);
1007
+ console.log('Gradient indicator added to the DOM');
950
1008
  }
951
1009
  function removeGradientAnimation() {
952
- const gradientDiv = document.querySelector("#gradient-indicator");
953
- if (gradientDiv) {
954
- gradientDiv.remove();
955
- console.log("Gradient indicator removed from the DOM");
956
- }
1010
+ const gradientDiv = document.querySelector('#gradient-indicator');
1011
+ if (gradientDiv) {
1012
+ gradientDiv.remove();
1013
+ console.log('Gradient indicator removed from the DOM');
1014
+ }
957
1015
  }
958
1016
 
959
- // dist/libs/core/src/lib/utils/is-browser.js
960
1017
  function isBrowser() {
961
- return typeof window !== "undefined" && typeof document !== "undefined";
1018
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
962
1019
  }
963
1020
 
964
- // dist/libs/core/src/lib/state-manager.js
965
- var StateManager = class {
966
- constructor() {
967
- this.state = "idle";
968
- this.subscribers = [];
969
- }
970
- getState() {
971
- return this.state;
972
- }
973
- setState(state) {
974
- this.state = state;
975
- this.notifySubscribers();
976
- console.log("State updated:", state);
977
- if (state === "listening") {
978
- addGradientAnimation();
979
- } else {
980
- removeGradientAnimation();
981
- }
982
- }
983
- // eslint-disable-next-line no-unused-vars
984
- subscribe(callback) {
985
- this.subscribers.push(callback);
986
- }
987
- notifySubscribers() {
988
- this.subscribers.forEach((callback) => callback(this.state));
989
- }
990
- };
1021
+ class StateManager {
1022
+ state = 'idle';
1023
+ // eslint-disable-next-line no-unused-vars
1024
+ subscribers = [];
1025
+ getState() {
1026
+ return this.state;
1027
+ }
1028
+ setState(state) {
1029
+ this.state = state;
1030
+ this.notifySubscribers();
1031
+ console.log('State updated:', state);
1032
+ // Dynamically update body class based on state
1033
+ if (state === 'listening') {
1034
+ addGradientAnimation();
1035
+ }
1036
+ else {
1037
+ removeGradientAnimation();
1038
+ }
1039
+ }
1040
+ // eslint-disable-next-line no-unused-vars
1041
+ subscribe(callback) {
1042
+ this.subscribers.push(callback);
1043
+ }
1044
+ notifySubscribers() {
1045
+ this.subscribers.forEach((callback) => callback(this.state));
1046
+ }
1047
+ }
991
1048
 
992
- // dist/libs/core/src/lib/ui/overlay-manager.js
993
- import { __awaiter as __awaiter4 } from "tslib";
994
- var OverlayManager = class {
995
- constructor(config) {
996
- this.container = null;
997
- this.chatWindow = null;
998
- this.messagesContainer = null;
999
- this.input = null;
1000
- this.isOpen = false;
1001
- this.loadingEl = null;
1002
- this.active = isBrowser();
1003
- this.config = config;
1004
- if (this.active)
1005
- this.init();
1006
- }
1007
- init() {
1008
- var _a, _b;
1009
- if (this.container)
1010
- return;
1011
- this.injectOverlayStyles();
1012
- const existing = document.getElementById("foisit-overlay-container");
1013
- if (existing && existing instanceof HTMLElement) {
1014
- this.container = existing;
1015
- this.chatWindow = existing.querySelector(".foisit-chat");
1016
- this.messagesContainer = existing.querySelector(".foisit-messages");
1017
- this.input = existing.querySelector("input.foisit-input");
1018
- if (((_a = this.config.floatingButton) === null || _a === void 0 ? void 0 : _a.visible) !== false && !existing.querySelector(".foisit-floating-btn")) {
1019
- this.renderFloatingButton();
1020
- }
1021
- if (!this.chatWindow) {
1049
+ /* eslint-disable no-unused-vars */
1050
+ class OverlayManager {
1051
+ container = null;
1052
+ chatWindow = null;
1053
+ messagesContainer = null;
1054
+ input = null;
1055
+ isOpen = false;
1056
+ onSubmit;
1057
+ onClose;
1058
+ loadingEl = null;
1059
+ config;
1060
+ active = isBrowser();
1061
+ constructor(config) {
1062
+ this.config = config;
1063
+ if (this.active)
1064
+ this.init();
1065
+ }
1066
+ init() {
1067
+ if (this.container)
1068
+ return;
1069
+ this.injectOverlayStyles();
1070
+ // Reuse an existing overlay container if one already exists on the page.
1071
+ // This prevents duplicate overlays when multiple assistant instances are created (e.g., React StrictMode).
1072
+ const existing = document.getElementById('foisit-overlay-container');
1073
+ if (existing && existing instanceof HTMLElement) {
1074
+ this.container = existing;
1075
+ this.chatWindow = existing.querySelector('.foisit-chat');
1076
+ this.messagesContainer = existing.querySelector('.foisit-messages');
1077
+ this.input = existing.querySelector('input.foisit-input');
1078
+ if (this.config.floatingButton?.visible !== false && !existing.querySelector('.foisit-floating-btn')) {
1079
+ this.renderFloatingButton();
1080
+ }
1081
+ if (!this.chatWindow) {
1082
+ this.renderChatWindow();
1083
+ }
1084
+ return;
1085
+ }
1086
+ this.container = document.createElement('div');
1087
+ this.container.id = 'foisit-overlay-container';
1088
+ this.container.className = 'foisit-overlay-container';
1089
+ document.body.appendChild(this.container);
1090
+ if (this.config.floatingButton?.visible !== false) {
1091
+ this.renderFloatingButton();
1092
+ }
1022
1093
  this.renderChatWindow();
1023
- }
1024
- return;
1025
- }
1026
- this.container = document.createElement("div");
1027
- this.container.id = "foisit-overlay-container";
1028
- this.container.className = "foisit-overlay-container";
1029
- document.body.appendChild(this.container);
1030
- if (((_b = this.config.floatingButton) === null || _b === void 0 ? void 0 : _b.visible) !== false) {
1031
- this.renderFloatingButton();
1032
- }
1033
- this.renderChatWindow();
1034
- }
1035
- renderFloatingButton() {
1036
- var _a, _b, _c, _d, _e, _f;
1037
- const btn = document.createElement("button");
1038
- btn.innerHTML = ((_a = this.config.floatingButton) === null || _a === void 0 ? void 0 : _a.customHtml) || "\u{1F399}\uFE0F";
1039
- const bottom = ((_c = (_b = this.config.floatingButton) === null || _b === void 0 ? void 0 : _b.position) === null || _c === void 0 ? void 0 : _c.bottom) || "20px";
1040
- const right = ((_e = (_d = this.config.floatingButton) === null || _d === void 0 ? void 0 : _d.position) === null || _e === void 0 ? void 0 : _e.right) || "20px";
1041
- btn.className = "foisit-floating-btn";
1042
- btn.style.bottom = bottom;
1043
- btn.style.right = right;
1044
- btn.onclick = () => this.toggle();
1045
- btn.onmouseenter = () => btn.style.transform = "scale(1.05)";
1046
- btn.onmouseleave = () => btn.style.transform = "scale(1)";
1047
- (_f = this.container) === null || _f === void 0 ? void 0 : _f.appendChild(btn);
1048
- }
1049
- renderChatWindow() {
1050
- var _a;
1051
- if (this.chatWindow)
1052
- return;
1053
- this.chatWindow = document.createElement("div");
1054
- this.chatWindow.className = "foisit-chat";
1055
- const header = document.createElement("div");
1056
- header.className = "foisit-header";
1057
- const title = document.createElement("span");
1058
- title.className = "foisit-title";
1059
- title.textContent = "Foisit";
1060
- const closeButton = document.createElement("button");
1061
- closeButton.type = "button";
1062
- closeButton.className = "foisit-close";
1063
- closeButton.setAttribute("aria-label", "Close");
1064
- closeButton.innerHTML = "&times;";
1065
- closeButton.addEventListener("click", () => this.toggle());
1066
- header.appendChild(title);
1067
- header.appendChild(closeButton);
1068
- this.messagesContainer = document.createElement("div");
1069
- this.messagesContainer.className = "foisit-messages";
1070
- const inputArea = document.createElement("div");
1071
- inputArea.className = "foisit-input-area";
1072
- this.input = document.createElement("input");
1073
- this.input.placeholder = this.config.inputPlaceholder || "Type a command...";
1074
- this.input.className = "foisit-input";
1075
- this.input.addEventListener("keydown", (e) => {
1076
- var _a2;
1077
- if (e.key === "Enter" && ((_a2 = this.input) === null || _a2 === void 0 ? void 0 : _a2.value.trim())) {
1078
- const text = this.input.value.trim();
1079
- this.input.value = "";
1080
- if (this.onSubmit)
1081
- this.onSubmit(text);
1082
- }
1083
- });
1084
- inputArea.appendChild(this.input);
1085
- this.chatWindow.appendChild(header);
1086
- this.chatWindow.appendChild(this.messagesContainer);
1087
- this.chatWindow.appendChild(inputArea);
1088
- (_a = this.container) === null || _a === void 0 ? void 0 : _a.appendChild(this.chatWindow);
1089
- }
1090
- registerCallbacks(onSubmit, onClose) {
1091
- if (!this.active)
1092
- return;
1093
- this.onSubmit = onSubmit;
1094
- this.onClose = onClose;
1095
- }
1096
- toggle(onSubmit, onClose) {
1097
- if (!this.active)
1098
- return;
1099
- if (onSubmit)
1100
- this.onSubmit = onSubmit;
1101
- if (onClose)
1102
- this.onClose = onClose;
1103
- this.isOpen = !this.isOpen;
1104
- if (this.chatWindow) {
1105
- if (this.isOpen) {
1106
- this.chatWindow.style.display = "flex";
1107
- requestAnimationFrame(() => {
1108
- if (this.chatWindow) {
1109
- this.chatWindow.style.opacity = "1";
1110
- this.chatWindow.style.transform = "translateY(0) scale(1)";
1111
- }
1112
- });
1113
- setTimeout(() => {
1114
- var _a;
1115
- return (_a = this.input) === null || _a === void 0 ? void 0 : _a.focus();
1116
- }, 100);
1117
- } else {
1118
- this.chatWindow.style.opacity = "0";
1119
- this.chatWindow.style.transform = "translateY(20px) scale(0.95)";
1120
- setTimeout(() => {
1121
- if (this.chatWindow && !this.isOpen) {
1122
- this.chatWindow.style.display = "none";
1123
- }
1124
- }, 200);
1125
- if (this.onClose)
1126
- this.onClose();
1127
- }
1128
1094
  }
1129
- }
1130
- addMessage(text, type) {
1131
- if (!this.messagesContainer)
1132
- return;
1133
- const msg = document.createElement("div");
1134
- msg.textContent = text;
1135
- msg.className = type === "user" ? "foisit-bubble user" : "foisit-bubble system";
1136
- this.messagesContainer.appendChild(msg);
1137
- this.scrollToBottom();
1138
- }
1139
- addOptions(options) {
1140
- if (!this.messagesContainer)
1141
- return;
1142
- const container = document.createElement("div");
1143
- container.className = "foisit-options-container";
1144
- options.forEach((opt) => {
1145
- const btn = document.createElement("button");
1146
- btn.textContent = opt.label;
1147
- btn.className = "foisit-option-chip";
1148
- btn.setAttribute("type", "button");
1149
- btn.setAttribute("aria-label", opt.label);
1150
- const clickPayload = () => {
1151
- if (opt.commandId) {
1152
- if (this.onSubmit)
1153
- this.onSubmit({ commandId: opt.commandId });
1154
- return;
1155
- }
1156
- const value = opt && typeof opt.value === "string" && opt.value.trim() ? opt.value : opt.label;
1157
- if (this.onSubmit)
1158
- this.onSubmit(value);
1159
- };
1160
- btn.onclick = clickPayload;
1161
- btn.onkeydown = (e) => {
1162
- if (e.key === "Enter" || e.key === " ") {
1163
- e.preventDefault();
1164
- clickPayload();
1165
- }
1166
- };
1167
- container.appendChild(btn);
1168
- });
1169
- this.messagesContainer.appendChild(container);
1170
- this.scrollToBottom();
1171
- }
1172
- addForm(message, fields, onSubmit) {
1173
- if (!this.messagesContainer)
1174
- return;
1175
- this.addMessage(message, "system");
1176
- const form = document.createElement("form");
1177
- form.className = "foisit-form";
1178
- const controls = [];
1179
- const createLabel = (text, required) => {
1180
- const label = document.createElement("div");
1181
- label.className = "foisit-form-label";
1182
- label.innerHTML = text + (required ? ' <span class="foisit-req-star">*</span>' : "");
1183
- return label;
1184
- };
1185
- const createError = () => {
1186
- const error = document.createElement("div");
1187
- error.className = "foisit-form-error";
1188
- error.style.display = "none";
1189
- return error;
1190
- };
1191
- (fields !== null && fields !== void 0 ? fields : []).forEach((field) => {
1192
- const wrapper = document.createElement("div");
1193
- wrapper.className = "foisit-form-group";
1194
- const labelText = field.description || field.name;
1195
- wrapper.appendChild(createLabel(labelText, field.required));
1196
- let inputEl;
1197
- if (field.type === "select") {
1198
- const select = document.createElement("select");
1199
- select.className = "foisit-form-input";
1200
- const placeholderOpt = document.createElement("option");
1201
- placeholderOpt.value = "";
1202
- placeholderOpt.textContent = "Select...";
1203
- select.appendChild(placeholderOpt);
1204
- const populate = (options) => {
1205
- (options !== null && options !== void 0 ? options : []).forEach((opt) => {
1206
- var _a, _b, _c, _d;
1207
- const o = document.createElement("option");
1208
- o.value = String((_b = (_a = opt.value) !== null && _a !== void 0 ? _a : opt.label) !== null && _b !== void 0 ? _b : "");
1209
- o.textContent = String((_d = (_c = opt.label) !== null && _c !== void 0 ? _c : opt.value) !== null && _d !== void 0 ? _d : "");
1210
- select.appendChild(o);
1211
- });
1212
- };
1213
- if (Array.isArray(field.options) && field.options.length) {
1214
- populate(field.options);
1215
- } else if (typeof field.getOptions === "function") {
1216
- const getOptions = field.getOptions;
1217
- const loadingOpt = document.createElement("option");
1218
- loadingOpt.value = "";
1219
- loadingOpt.textContent = "Loading...";
1220
- select.appendChild(loadingOpt);
1221
- Promise.resolve().then(() => getOptions()).then((opts) => {
1222
- while (select.options.length > 1)
1223
- select.remove(1);
1224
- populate(opts);
1225
- }).catch(() => {
1226
- while (select.options.length > 1)
1227
- select.remove(1);
1228
- const errOpt = document.createElement("option");
1229
- errOpt.value = "";
1230
- errOpt.textContent = "Error loading options";
1231
- select.appendChild(errOpt);
1232
- });
1233
- }
1234
- if (field.defaultValue != null) {
1235
- select.value = String(field.defaultValue);
1236
- }
1237
- inputEl = select;
1238
- } else if (field.type === "file") {
1239
- const ffield = field;
1240
- const input = document.createElement("input");
1241
- input.className = "foisit-form-input";
1242
- input.type = "file";
1243
- if (ffield.accept && Array.isArray(ffield.accept)) {
1244
- input.accept = ffield.accept.join(",");
1245
- }
1246
- if (ffield.multiple)
1247
- input.multiple = true;
1248
- if (ffield.capture) {
1249
- if (ffield.capture === true)
1250
- input.setAttribute("capture", "");
1251
- else
1252
- input.setAttribute("capture", String(ffield.capture));
1253
- }
1254
- input.addEventListener("change", () => __awaiter4(this, void 0, void 0, function* () {
1255
- var _a, _b, _c;
1256
- const files = Array.from(input.files || []);
1257
- const errEl = errorEl;
1258
- errEl.style.display = "none";
1259
- errEl.textContent = "";
1260
- if (files.length === 0)
1095
+ renderFloatingButton() {
1096
+ const btn = document.createElement('button');
1097
+ btn.innerHTML = this.config.floatingButton?.customHtml || '🎙️';
1098
+ const bottom = this.config.floatingButton?.position?.bottom || '20px';
1099
+ const right = this.config.floatingButton?.position?.right || '20px';
1100
+ btn.className = 'foisit-floating-btn';
1101
+ btn.style.bottom = bottom;
1102
+ btn.style.right = right;
1103
+ btn.onclick = () => this.toggle();
1104
+ btn.onmouseenter = () => (btn.style.transform = 'scale(1.05)');
1105
+ btn.onmouseleave = () => (btn.style.transform = 'scale(1)');
1106
+ this.container?.appendChild(btn);
1107
+ }
1108
+ renderChatWindow() {
1109
+ if (this.chatWindow)
1261
1110
  return;
1262
- const maxFiles = (_a = ffield.maxFiles) !== null && _a !== void 0 ? _a : ffield.multiple ? 10 : 1;
1263
- if (files.length > maxFiles) {
1264
- errEl.textContent = `Please select at most ${maxFiles} file(s).`;
1265
- errEl.style.display = "block";
1111
+ this.chatWindow = document.createElement('div');
1112
+ this.chatWindow.className = 'foisit-chat';
1113
+ // Header
1114
+ const header = document.createElement('div');
1115
+ header.className = 'foisit-header';
1116
+ const title = document.createElement('span');
1117
+ title.className = 'foisit-title';
1118
+ title.textContent = 'Foisit';
1119
+ const closeButton = document.createElement('button');
1120
+ closeButton.type = 'button';
1121
+ closeButton.className = 'foisit-close';
1122
+ closeButton.setAttribute('aria-label', 'Close');
1123
+ closeButton.innerHTML = '&times;';
1124
+ closeButton.addEventListener('click', () => this.toggle());
1125
+ header.appendChild(title);
1126
+ header.appendChild(closeButton);
1127
+ // Messages Area
1128
+ this.messagesContainer = document.createElement('div');
1129
+ this.messagesContainer.className = 'foisit-messages';
1130
+ // Input Area
1131
+ const inputArea = document.createElement('div');
1132
+ inputArea.className = 'foisit-input-area';
1133
+ this.input = document.createElement('input');
1134
+ this.input.placeholder =
1135
+ this.config.inputPlaceholder || 'Type a command...';
1136
+ this.input.className = 'foisit-input';
1137
+ this.input.addEventListener('keydown', (e) => {
1138
+ if (e.key === 'Enter' && this.input?.value.trim()) {
1139
+ const text = this.input.value.trim();
1140
+ this.input.value = '';
1141
+ if (this.onSubmit)
1142
+ this.onSubmit(text);
1143
+ }
1144
+ });
1145
+ inputArea.appendChild(this.input);
1146
+ this.chatWindow.appendChild(header);
1147
+ this.chatWindow.appendChild(this.messagesContainer);
1148
+ this.chatWindow.appendChild(inputArea);
1149
+ this.container?.appendChild(this.chatWindow);
1150
+ }
1151
+ registerCallbacks(onSubmit, onClose) {
1152
+ if (!this.active)
1153
+ return; // no-op on server
1154
+ this.onSubmit = onSubmit;
1155
+ this.onClose = onClose;
1156
+ }
1157
+ toggle(onSubmit, onClose) {
1158
+ if (!this.active)
1159
+ return; // no-op on server
1160
+ if (onSubmit)
1161
+ this.onSubmit = onSubmit;
1162
+ if (onClose)
1163
+ this.onClose = onClose;
1164
+ this.isOpen = !this.isOpen;
1165
+ if (this.chatWindow) {
1166
+ if (this.isOpen) {
1167
+ this.chatWindow.style.display = 'flex';
1168
+ requestAnimationFrame(() => {
1169
+ if (this.chatWindow) {
1170
+ this.chatWindow.style.opacity = '1';
1171
+ this.chatWindow.style.transform = 'translateY(0) scale(1)';
1172
+ }
1173
+ });
1174
+ setTimeout(() => this.input?.focus(), 100);
1175
+ }
1176
+ else {
1177
+ this.chatWindow.style.opacity = '0';
1178
+ this.chatWindow.style.transform = 'translateY(20px) scale(0.95)';
1179
+ setTimeout(() => {
1180
+ if (this.chatWindow && !this.isOpen) {
1181
+ this.chatWindow.style.display = 'none';
1182
+ }
1183
+ }, 200);
1184
+ if (this.onClose)
1185
+ this.onClose();
1186
+ }
1187
+ }
1188
+ }
1189
+ addMessage(text, type) {
1190
+ if (!this.messagesContainer)
1266
1191
  return;
1267
- }
1268
- const maxSize = (_b = ffield.maxSizeBytes) !== null && _b !== void 0 ? _b : Infinity;
1269
- const total = files.reduce((s, f) => s + f.size, 0);
1270
- if (files.some((f) => f.size > maxSize)) {
1271
- errEl.textContent = `One or more files exceed the maximum size of ${Math.round(maxSize / 1024)} KB.`;
1272
- errEl.style.display = "block";
1192
+ const msg = document.createElement('div');
1193
+ msg.textContent = text;
1194
+ msg.className = type === 'user' ? 'foisit-bubble user' : 'foisit-bubble system';
1195
+ this.messagesContainer.appendChild(msg);
1196
+ this.scrollToBottom();
1197
+ }
1198
+ addOptions(options) {
1199
+ if (!this.messagesContainer)
1273
1200
  return;
1274
- }
1275
- const maxTotal = (_c = ffield.maxTotalBytes) !== null && _c !== void 0 ? _c : Infinity;
1276
- if (total > maxTotal) {
1277
- errEl.textContent = `Total selected files exceed the maximum of ${Math.round(maxTotal / 1024)} KB.`;
1278
- errEl.style.display = "block";
1201
+ const container = document.createElement('div');
1202
+ container.className = 'foisit-options-container';
1203
+ options.forEach((opt) => {
1204
+ const btn = document.createElement('button');
1205
+ btn.textContent = opt.label;
1206
+ btn.className = 'foisit-option-chip';
1207
+ btn.setAttribute('type', 'button');
1208
+ btn.setAttribute('aria-label', opt.label);
1209
+ const clickPayload = () => {
1210
+ // If commandId is provided, submit a structured payload so the handler
1211
+ // can run it deterministically: { commandId, params? }
1212
+ if (opt.commandId) {
1213
+ if (this.onSubmit)
1214
+ this.onSubmit({ commandId: opt.commandId });
1215
+ return;
1216
+ }
1217
+ // Otherwise fall back to value or label string
1218
+ const value = (opt && typeof opt.value === 'string' && opt.value.trim()) ? opt.value : opt.label;
1219
+ if (this.onSubmit)
1220
+ this.onSubmit(value);
1221
+ };
1222
+ btn.onclick = clickPayload;
1223
+ btn.onkeydown = (e) => {
1224
+ if (e.key === 'Enter' || e.key === ' ') {
1225
+ e.preventDefault();
1226
+ clickPayload();
1227
+ }
1228
+ };
1229
+ container.appendChild(btn);
1230
+ });
1231
+ this.messagesContainer.appendChild(container);
1232
+ this.scrollToBottom();
1233
+ }
1234
+ addForm(message, fields, onSubmit) {
1235
+ if (!this.messagesContainer)
1279
1236
  return;
1280
- }
1281
- if (ffield.accept && Array.isArray(ffield.accept)) {
1282
- const accepts = ffield.accept;
1283
- const ok = files.every((file) => {
1284
- if (!file.type)
1285
- return true;
1286
- return accepts.some((a) => a.startsWith(".") ? file.name.toLowerCase().endsWith(a.toLowerCase()) : file.type === a || file.type.startsWith(a.split("/")[0] + "/"));
1287
- });
1288
- if (!ok) {
1289
- errEl.textContent = "One or more files have an unsupported type.";
1290
- errEl.style.display = "block";
1291
- return;
1237
+ this.addMessage(message, 'system');
1238
+ const form = document.createElement('form');
1239
+ form.className = 'foisit-form';
1240
+ const controls = [];
1241
+ const createLabel = (text, required) => {
1242
+ const label = document.createElement('div');
1243
+ label.className = 'foisit-form-label';
1244
+ label.innerHTML = text + (required ? ' <span class="foisit-req-star">*</span>' : '');
1245
+ return label;
1246
+ };
1247
+ const createError = () => {
1248
+ const error = document.createElement('div');
1249
+ error.className = 'foisit-form-error';
1250
+ error.style.display = 'none';
1251
+ return error;
1252
+ };
1253
+ (fields ?? []).forEach((field) => {
1254
+ const wrapper = document.createElement('div');
1255
+ wrapper.className = 'foisit-form-group';
1256
+ const labelText = field.description || field.name;
1257
+ wrapper.appendChild(createLabel(labelText, field.required));
1258
+ let inputEl;
1259
+ if (field.type === 'select') {
1260
+ const select = document.createElement('select');
1261
+ select.className = 'foisit-form-input';
1262
+ const placeholderOpt = document.createElement('option');
1263
+ placeholderOpt.value = '';
1264
+ placeholderOpt.textContent = 'Select...';
1265
+ select.appendChild(placeholderOpt);
1266
+ const populate = (options) => {
1267
+ (options ?? []).forEach((opt) => {
1268
+ const o = document.createElement('option');
1269
+ o.value = String(opt.value ?? opt.label ?? '');
1270
+ o.textContent = String(opt.label ?? opt.value ?? '');
1271
+ select.appendChild(o);
1272
+ });
1273
+ };
1274
+ if (Array.isArray(field.options) && field.options.length) {
1275
+ populate(field.options);
1276
+ }
1277
+ else if (typeof field.getOptions === 'function') {
1278
+ const getOptions = field.getOptions;
1279
+ const loadingOpt = document.createElement('option');
1280
+ loadingOpt.value = '';
1281
+ loadingOpt.textContent = 'Loading...';
1282
+ select.appendChild(loadingOpt);
1283
+ Promise.resolve()
1284
+ .then(() => getOptions())
1285
+ .then((opts) => {
1286
+ while (select.options.length > 1)
1287
+ select.remove(1);
1288
+ populate(opts);
1289
+ })
1290
+ .catch(() => {
1291
+ while (select.options.length > 1)
1292
+ select.remove(1);
1293
+ const errOpt = document.createElement('option');
1294
+ errOpt.value = '';
1295
+ errOpt.textContent = 'Error loading options';
1296
+ select.appendChild(errOpt);
1297
+ });
1298
+ }
1299
+ if (field.defaultValue != null) {
1300
+ select.value = String(field.defaultValue);
1301
+ }
1302
+ inputEl = select;
1292
1303
  }
1293
- }
1294
- }));
1295
- inputEl = input;
1296
- } else {
1297
- const input = document.createElement("input");
1298
- input.className = "foisit-form-input";
1299
- if (field.type === "string") {
1300
- input.placeholder = field.placeholder || "Type here...";
1301
- }
1302
- if (field.type === "number") {
1303
- input.type = "number";
1304
- if (typeof field.min === "number")
1305
- input.min = String(field.min);
1306
- if (typeof field.max === "number")
1307
- input.max = String(field.max);
1308
- if (typeof field.step === "number")
1309
- input.step = String(field.step);
1310
- if (field.defaultValue != null)
1311
- input.value = String(field.defaultValue);
1312
- } else if (field.type === "date") {
1313
- input.type = "date";
1314
- if (typeof field.min === "string")
1315
- input.min = field.min;
1316
- if (typeof field.max === "string")
1317
- input.max = field.max;
1318
- if (field.defaultValue != null)
1319
- input.value = String(field.defaultValue);
1320
- } else {
1321
- input.type = "text";
1322
- if (field.defaultValue != null)
1323
- input.value = String(field.defaultValue);
1324
- }
1325
- inputEl = input;
1326
- }
1327
- const errorEl = createError();
1328
- wrapper.appendChild(inputEl);
1329
- wrapper.appendChild(errorEl);
1330
- controls.push({
1331
- name: field.name,
1332
- type: field.type,
1333
- el: inputEl,
1334
- required: field.required
1335
- });
1336
- form.appendChild(wrapper);
1337
- });
1338
- const actions = document.createElement("div");
1339
- actions.className = "foisit-form-actions";
1340
- const submitBtn = document.createElement("button");
1341
- submitBtn.type = "submit";
1342
- submitBtn.textContent = "Submit";
1343
- submitBtn.className = "foisit-option-chip";
1344
- submitBtn.style.fontWeight = "600";
1345
- actions.appendChild(submitBtn);
1346
- form.appendChild(actions);
1347
- form.onsubmit = (e) => __awaiter4(this, void 0, void 0, function* () {
1348
- var _a, _b;
1349
- e.preventDefault();
1350
- const data = {};
1351
- let hasError = false;
1352
- form.querySelectorAll(".foisit-form-error").forEach((el) => {
1353
- el.style.display = "none";
1354
- el.textContent = "";
1355
- });
1356
- form.querySelectorAll(".foisit-form-input").forEach((el) => {
1357
- el.classList.remove("foisit-error-border");
1358
- });
1359
- for (const c of controls) {
1360
- if (c.type === "file") {
1361
- const fileWrapper = c.el.parentElement;
1362
- const fileErrorEl = fileWrapper === null || fileWrapper === void 0 ? void 0 : fileWrapper.querySelector(".foisit-form-error");
1363
- const input = c.el;
1364
- const files = Array.from(input.files || []);
1365
- if (c.required && files.length === 0) {
1366
- hasError = true;
1367
- input.classList.add("foisit-error-border");
1368
- if (fileErrorEl) {
1369
- fileErrorEl.textContent = "This file is required";
1370
- fileErrorEl.style.display = "block";
1304
+ else if (field.type === 'file') {
1305
+ const ffield = field;
1306
+ const input = document.createElement('input');
1307
+ input.className = 'foisit-form-input';
1308
+ input.type = 'file';
1309
+ if (ffield.accept && Array.isArray(ffield.accept)) {
1310
+ input.accept = ffield.accept.join(',');
1311
+ }
1312
+ if (ffield.multiple)
1313
+ input.multiple = true;
1314
+ if (ffield.capture) {
1315
+ if (ffield.capture === true)
1316
+ input.setAttribute('capture', '');
1317
+ else
1318
+ input.setAttribute('capture', String(ffield.capture));
1319
+ }
1320
+ // Validation state stored on the element via dataset
1321
+ input.addEventListener('change', async () => {
1322
+ const files = Array.from(input.files || []);
1323
+ const errEl = errorEl;
1324
+ errEl.style.display = 'none';
1325
+ errEl.textContent = '';
1326
+ if (files.length === 0)
1327
+ return;
1328
+ // Basic validations: count and sizes
1329
+ const maxFiles = ffield.maxFiles ?? (ffield.multiple ? 10 : 1);
1330
+ if (files.length > maxFiles) {
1331
+ errEl.textContent = `Please select at most ${maxFiles} file(s).`;
1332
+ errEl.style.display = 'block';
1333
+ return;
1334
+ }
1335
+ const maxSize = ffield.maxSizeBytes ?? Infinity;
1336
+ const total = files.reduce((s, f) => s + f.size, 0);
1337
+ if (files.some(f => f.size > maxSize)) {
1338
+ errEl.textContent = `One or more files exceed the maximum size of ${Math.round(maxSize / 1024)} KB.`;
1339
+ errEl.style.display = 'block';
1340
+ return;
1341
+ }
1342
+ const maxTotal = ffield.maxTotalBytes ?? Infinity;
1343
+ if (total > maxTotal) {
1344
+ errEl.textContent = `Total selected files exceed the maximum of ${Math.round(maxTotal / 1024)} KB.`;
1345
+ errEl.style.display = 'block';
1346
+ return;
1347
+ }
1348
+ // Basic mime/extension check
1349
+ if (ffield.accept && Array.isArray(ffield.accept)) {
1350
+ const accepts = ffield.accept;
1351
+ const ok = files.every((file) => {
1352
+ if (!file.type)
1353
+ return true; // can't tell
1354
+ return accepts.some(a => a.startsWith('.') ? file.name.toLowerCase().endsWith(a.toLowerCase()) : file.type === a || file.type.startsWith(a.split('/')[0] + '/'));
1355
+ });
1356
+ if (!ok) {
1357
+ errEl.textContent = 'One or more files have an unsupported type.';
1358
+ errEl.style.display = 'block';
1359
+ return;
1360
+ }
1361
+ }
1362
+ });
1363
+ inputEl = input;
1371
1364
  }
1372
- continue;
1373
- }
1374
- if (files.length === 0)
1375
- continue;
1376
- const fieldDef = (fields !== null && fields !== void 0 ? fields : []).find((f) => f.name === c.name);
1377
- const delivery = (_a = fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.delivery) !== null && _a !== void 0 ? _a : "file";
1378
- if ((fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.maxWidth) || (fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.maxHeight)) {
1379
- try {
1380
- const dims = yield this.getImageDimensions(files[0]);
1381
- if (fieldDef.maxWidth && dims.width > fieldDef.maxWidth) {
1382
- hasError = true;
1383
- if (fileErrorEl) {
1384
- fileErrorEl.textContent = `Image width must be \u2264 ${fieldDef.maxWidth}px`;
1385
- fileErrorEl.style.display = "block";
1365
+ else {
1366
+ const input = document.createElement('input');
1367
+ input.className = 'foisit-form-input';
1368
+ if (field.type === 'string') {
1369
+ input.placeholder = field.placeholder || 'Type here...';
1386
1370
  }
1387
- continue;
1388
- }
1389
- if (fieldDef.maxHeight && dims.height > fieldDef.maxHeight) {
1390
- hasError = true;
1391
- if (fileErrorEl) {
1392
- fileErrorEl.textContent = `Image height must be \u2264 ${fieldDef.maxHeight}px`;
1393
- fileErrorEl.style.display = "block";
1371
+ if (field.type === 'number') {
1372
+ input.type = 'number';
1373
+ if (typeof field.min === 'number')
1374
+ input.min = String(field.min);
1375
+ if (typeof field.max === 'number')
1376
+ input.max = String(field.max);
1377
+ if (typeof field.step === 'number')
1378
+ input.step = String(field.step);
1379
+ if (field.defaultValue != null)
1380
+ input.value = String(field.defaultValue);
1394
1381
  }
1395
- continue;
1396
- }
1397
- } catch (_c) {
1382
+ else if (field.type === 'date') {
1383
+ input.type = 'date';
1384
+ if (typeof field.min === 'string')
1385
+ input.min = field.min;
1386
+ if (typeof field.max === 'string')
1387
+ input.max = field.max;
1388
+ if (field.defaultValue != null)
1389
+ input.value = String(field.defaultValue);
1390
+ }
1391
+ else {
1392
+ input.type = 'text';
1393
+ if (field.defaultValue != null)
1394
+ input.value = String(field.defaultValue);
1395
+ }
1396
+ inputEl = input;
1398
1397
  }
1399
- }
1400
- if (fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.maxDurationSec) {
1401
- try {
1402
- const dur = yield this.getMediaDuration(files[0]);
1403
- if (dur && dur > fieldDef.maxDurationSec) {
1404
- hasError = true;
1405
- if (fileErrorEl) {
1406
- fileErrorEl.textContent = `Media duration must be \u2264 ${fieldDef.maxDurationSec}s`;
1407
- fileErrorEl.style.display = "block";
1398
+ // Add Error element
1399
+ const errorEl = createError();
1400
+ wrapper.appendChild(inputEl);
1401
+ wrapper.appendChild(errorEl); // Append error container
1402
+ controls.push({
1403
+ name: field.name,
1404
+ type: field.type,
1405
+ el: inputEl,
1406
+ required: field.required,
1407
+ });
1408
+ form.appendChild(wrapper);
1409
+ });
1410
+ const actions = document.createElement('div');
1411
+ actions.className = 'foisit-form-actions';
1412
+ const submitBtn = document.createElement('button');
1413
+ submitBtn.type = 'submit';
1414
+ submitBtn.textContent = 'Submit';
1415
+ submitBtn.className = 'foisit-option-chip';
1416
+ submitBtn.style.fontWeight = '600';
1417
+ actions.appendChild(submitBtn);
1418
+ form.appendChild(actions);
1419
+ form.onsubmit = async (e) => {
1420
+ e.preventDefault();
1421
+ const data = {};
1422
+ let hasError = false;
1423
+ // Clear previous errors
1424
+ form.querySelectorAll('.foisit-form-error').forEach(el => {
1425
+ el.style.display = 'none';
1426
+ el.textContent = '';
1427
+ });
1428
+ form.querySelectorAll('.foisit-form-input').forEach(el => {
1429
+ el.classList.remove('foisit-error-border');
1430
+ });
1431
+ for (const c of controls) {
1432
+ // FILE inputs need special handling
1433
+ if (c.type === 'file') {
1434
+ const fileWrapper = c.el.parentElement;
1435
+ const fileErrorEl = fileWrapper?.querySelector('.foisit-form-error');
1436
+ const input = c.el;
1437
+ const files = Array.from(input.files || []);
1438
+ if (c.required && files.length === 0) {
1439
+ hasError = true;
1440
+ input.classList.add('foisit-error-border');
1441
+ if (fileErrorEl) {
1442
+ fileErrorEl.textContent = 'This file is required';
1443
+ fileErrorEl.style.display = 'block';
1444
+ }
1445
+ continue;
1446
+ }
1447
+ if (files.length === 0)
1448
+ continue;
1449
+ // Find corresponding field definition
1450
+ const fieldDef = (fields ?? []).find((f) => f.name === c.name);
1451
+ const delivery = fieldDef?.delivery ?? 'file';
1452
+ // Run optional dimension/duration checks (best-effort)
1453
+ if (fieldDef?.maxWidth || fieldDef?.maxHeight) {
1454
+ // check first image only
1455
+ try {
1456
+ const dims = await this.getImageDimensions(files[0]);
1457
+ if (fieldDef.maxWidth && dims.width > fieldDef.maxWidth) {
1458
+ hasError = true;
1459
+ if (fileErrorEl) {
1460
+ fileErrorEl.textContent = `Image width must be ≤ ${fieldDef.maxWidth}px`;
1461
+ fileErrorEl.style.display = 'block';
1462
+ }
1463
+ continue;
1464
+ }
1465
+ if (fieldDef.maxHeight && dims.height > fieldDef.maxHeight) {
1466
+ hasError = true;
1467
+ if (fileErrorEl) {
1468
+ fileErrorEl.textContent = `Image height must be ≤ ${fieldDef.maxHeight}px`;
1469
+ fileErrorEl.style.display = 'block';
1470
+ }
1471
+ continue;
1472
+ }
1473
+ }
1474
+ catch {
1475
+ // ignore dimension check failures
1476
+ }
1477
+ }
1478
+ if (fieldDef?.maxDurationSec) {
1479
+ try {
1480
+ const dur = await this.getMediaDuration(files[0]);
1481
+ if (dur && dur > fieldDef.maxDurationSec) {
1482
+ hasError = true;
1483
+ if (fileErrorEl) {
1484
+ fileErrorEl.textContent = `Media duration must be ≤ ${fieldDef.maxDurationSec}s`;
1485
+ fileErrorEl.style.display = 'block';
1486
+ }
1487
+ continue;
1488
+ }
1489
+ }
1490
+ catch {
1491
+ // ignore
1492
+ }
1493
+ }
1494
+ // Prepare payload according to delivery
1495
+ if (delivery === 'file') {
1496
+ data[c.name] = fieldDef?.multiple ? files : files[0];
1497
+ }
1498
+ else if (delivery === 'base64') {
1499
+ try {
1500
+ const encoded = await Promise.all(files.map((file) => this.readFileAsDataURL(file)));
1501
+ data[c.name] = fieldDef?.multiple ? encoded : encoded[0];
1502
+ }
1503
+ catch {
1504
+ hasError = true;
1505
+ if (fileErrorEl) {
1506
+ fileErrorEl.textContent = 'Failed to encode file(s) to base64.';
1507
+ fileErrorEl.style.display = 'block';
1508
+ }
1509
+ continue;
1510
+ }
1511
+ }
1512
+ continue;
1513
+ }
1514
+ const val = (c.el.value ?? '').toString().trim();
1515
+ const valueWrapper = c.el.parentElement;
1516
+ const fieldErrorEl = valueWrapper?.querySelector('.foisit-form-error');
1517
+ if (c.required && (val == null || val === '')) {
1518
+ hasError = true;
1519
+ c.el.classList.add('foisit-error-border');
1520
+ if (fieldErrorEl) {
1521
+ fieldErrorEl.textContent = 'This field is required';
1522
+ fieldErrorEl.style.display = 'block';
1523
+ }
1524
+ continue;
1525
+ }
1526
+ if (val === '')
1527
+ continue;
1528
+ if (c.type === 'number') {
1529
+ const n = Number(val);
1530
+ if (!Number.isNaN(n))
1531
+ data[c.name] = n;
1532
+ }
1533
+ else {
1534
+ data[c.name] = val;
1408
1535
  }
1409
- continue;
1410
- }
1411
- } catch (_d) {
1412
1536
  }
1413
- }
1414
- if (delivery === "file") {
1415
- data[c.name] = (fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.multiple) ? files : files[0];
1416
- } else if (delivery === "base64") {
1417
- try {
1418
- const encoded = yield Promise.all(files.map((file) => this.readFileAsDataURL(file)));
1419
- data[c.name] = (fieldDef === null || fieldDef === void 0 ? void 0 : fieldDef.multiple) ? encoded : encoded[0];
1420
- } catch (_e) {
1421
- hasError = true;
1422
- if (fileErrorEl) {
1423
- fileErrorEl.textContent = "Failed to encode file(s) to base64.";
1424
- fileErrorEl.style.display = "block";
1425
- }
1426
- continue;
1537
+ if (hasError) {
1538
+ // Shake animation
1539
+ form.classList.add('foisit-shake');
1540
+ setTimeout(() => form.classList.remove('foisit-shake'), 400);
1541
+ return;
1427
1542
  }
1428
- }
1429
- continue;
1430
- }
1431
- const val = ((_b = c.el.value) !== null && _b !== void 0 ? _b : "").toString().trim();
1432
- const valueWrapper = c.el.parentElement;
1433
- const fieldErrorEl = valueWrapper === null || valueWrapper === void 0 ? void 0 : valueWrapper.querySelector(".foisit-form-error");
1434
- if (c.required && (val == null || val === "")) {
1435
- hasError = true;
1436
- c.el.classList.add("foisit-error-border");
1437
- if (fieldErrorEl) {
1438
- fieldErrorEl.textContent = "This field is required";
1439
- fieldErrorEl.style.display = "block";
1440
- }
1441
- continue;
1442
- }
1443
- if (val === "")
1444
- continue;
1445
- if (c.type === "number") {
1446
- const n = Number(val);
1447
- if (!Number.isNaN(n))
1448
- data[c.name] = n;
1449
- } else {
1450
- data[c.name] = val;
1451
- }
1452
- }
1453
- if (hasError) {
1454
- form.classList.add("foisit-shake");
1455
- setTimeout(() => form.classList.remove("foisit-shake"), 400);
1456
- return;
1457
- }
1458
- submitBtn.disabled = true;
1459
- submitBtn.style.opacity = "0.6";
1460
- controls.forEach((c) => {
1461
- c.el.disabled = true;
1462
- });
1463
- onSubmit(data);
1464
- });
1465
- this.messagesContainer.appendChild(form);
1466
- this.scrollToBottom();
1467
- }
1468
- showLoading() {
1469
- if (!this.messagesContainer)
1470
- return;
1471
- if (this.loadingEl)
1472
- return;
1473
- this.loadingEl = document.createElement("div");
1474
- this.loadingEl.className = "foisit-loading-dots foisit-bubble system";
1475
- for (let i = 0; i < 3; i++) {
1476
- const dot = document.createElement("div");
1477
- dot.className = "foisit-dot";
1478
- dot.style.animation = `foisitPulse 1.4s infinite ease-in-out ${i * 0.2}s`;
1479
- this.loadingEl.appendChild(dot);
1480
- }
1481
- this.messagesContainer.appendChild(this.loadingEl);
1482
- this.scrollToBottom();
1483
- }
1484
- hideLoading() {
1485
- var _a;
1486
- (_a = this.loadingEl) === null || _a === void 0 ? void 0 : _a.remove();
1487
- this.loadingEl = null;
1488
- }
1489
- scrollToBottom() {
1490
- if (this.messagesContainer) {
1491
- this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
1492
- }
1493
- }
1494
- destroy() {
1495
- var _a;
1496
- (_a = this.container) === null || _a === void 0 ? void 0 : _a.remove();
1497
- this.container = null;
1498
- this.chatWindow = null;
1499
- this.messagesContainer = null;
1500
- this.input = null;
1501
- this.isOpen = false;
1502
- }
1503
- readFileAsDataURL(file) {
1504
- return new Promise((resolve, reject) => {
1505
- const fr = new FileReader();
1506
- fr.onerror = () => reject(new Error("Failed to read file"));
1507
- fr.onload = () => resolve(String(fr.result));
1508
- fr.readAsDataURL(file);
1509
- });
1510
- }
1511
- getImageDimensions(file) {
1512
- return new Promise((resolve) => {
1513
- try {
1514
- const url = URL.createObjectURL(file);
1515
- const img = new Image();
1516
- img.onload = () => {
1517
- const dims = { width: img.naturalWidth || img.width, height: img.naturalHeight || img.height };
1518
- URL.revokeObjectURL(url);
1519
- resolve(dims);
1520
- };
1521
- img.onerror = () => {
1522
- URL.revokeObjectURL(url);
1523
- resolve({ width: 0, height: 0 });
1543
+ submitBtn.disabled = true;
1544
+ submitBtn.style.opacity = '0.6';
1545
+ controls.forEach((c) => {
1546
+ c.el.disabled = true;
1547
+ });
1548
+ onSubmit(data);
1524
1549
  };
1525
- img.src = url;
1526
- } catch (_a) {
1527
- resolve({ width: 0, height: 0 });
1528
- }
1529
- });
1530
- }
1531
- getMediaDuration(file) {
1532
- return new Promise((resolve) => {
1533
- try {
1534
- const url = URL.createObjectURL(file);
1535
- const el = file.type.startsWith("audio") ? document.createElement("audio") : document.createElement("video");
1536
- let settled = false;
1537
- const timeout = setTimeout(() => {
1538
- if (!settled) {
1539
- settled = true;
1540
- URL.revokeObjectURL(url);
1541
- resolve(0);
1542
- }
1543
- }, 5e3);
1544
- el.preload = "metadata";
1545
- el.onloadedmetadata = () => {
1546
- if (settled)
1550
+ this.messagesContainer.appendChild(form);
1551
+ this.scrollToBottom();
1552
+ }
1553
+ showLoading() {
1554
+ if (!this.messagesContainer)
1547
1555
  return;
1548
- settled = true;
1549
- clearTimeout(timeout);
1550
- const mediaEl = el;
1551
- const d = mediaEl.duration || 0;
1552
- URL.revokeObjectURL(url);
1553
- resolve(d);
1554
- };
1555
- el.onerror = () => {
1556
- if (settled)
1556
+ if (this.loadingEl)
1557
1557
  return;
1558
- settled = true;
1559
- clearTimeout(timeout);
1560
- URL.revokeObjectURL(url);
1561
- resolve(0);
1562
- };
1563
- el.src = url;
1564
- } catch (_a) {
1565
- resolve(0);
1566
- }
1567
- });
1568
- }
1569
- injectOverlayStyles() {
1570
- if (document.getElementById("foisit-overlay-styles"))
1571
- return;
1572
- const style = document.createElement("style");
1573
- style.id = "foisit-overlay-styles";
1574
- style.textContent = `
1558
+ this.loadingEl = document.createElement('div');
1559
+ this.loadingEl.className = 'foisit-loading-dots foisit-bubble system';
1560
+ // Create dots
1561
+ for (let i = 0; i < 3; i++) {
1562
+ const dot = document.createElement('div');
1563
+ dot.className = 'foisit-dot';
1564
+ dot.style.animation = `foisitPulse 1.4s infinite ease-in-out ${i * 0.2}s`;
1565
+ this.loadingEl.appendChild(dot);
1566
+ }
1567
+ this.messagesContainer.appendChild(this.loadingEl);
1568
+ this.scrollToBottom();
1569
+ }
1570
+ hideLoading() {
1571
+ this.loadingEl?.remove();
1572
+ this.loadingEl = null;
1573
+ }
1574
+ scrollToBottom() {
1575
+ if (this.messagesContainer) {
1576
+ this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight;
1577
+ }
1578
+ }
1579
+ destroy() {
1580
+ this.container?.remove();
1581
+ this.container = null;
1582
+ this.chatWindow = null;
1583
+ this.messagesContainer = null;
1584
+ this.input = null;
1585
+ this.isOpen = false;
1586
+ }
1587
+ readFileAsDataURL(file) {
1588
+ return new Promise((resolve, reject) => {
1589
+ const fr = new FileReader();
1590
+ fr.onerror = () => reject(new Error('Failed to read file'));
1591
+ fr.onload = () => resolve(String(fr.result));
1592
+ fr.readAsDataURL(file);
1593
+ });
1594
+ }
1595
+ getImageDimensions(file) {
1596
+ return new Promise((resolve) => {
1597
+ try {
1598
+ const url = URL.createObjectURL(file);
1599
+ const img = new Image();
1600
+ img.onload = () => {
1601
+ const dims = { width: img.naturalWidth || img.width, height: img.naturalHeight || img.height };
1602
+ URL.revokeObjectURL(url);
1603
+ resolve(dims);
1604
+ };
1605
+ img.onerror = () => {
1606
+ URL.revokeObjectURL(url);
1607
+ resolve({ width: 0, height: 0 });
1608
+ };
1609
+ img.src = url;
1610
+ }
1611
+ catch {
1612
+ resolve({ width: 0, height: 0 });
1613
+ }
1614
+ });
1615
+ }
1616
+ getMediaDuration(file) {
1617
+ return new Promise((resolve) => {
1618
+ try {
1619
+ const url = URL.createObjectURL(file);
1620
+ const el = file.type.startsWith('audio') ? document.createElement('audio') : document.createElement('video');
1621
+ let settled = false;
1622
+ const timeout = setTimeout(() => {
1623
+ if (!settled) {
1624
+ settled = true;
1625
+ URL.revokeObjectURL(url);
1626
+ resolve(0);
1627
+ }
1628
+ }, 5000);
1629
+ el.preload = 'metadata';
1630
+ el.onloadedmetadata = () => {
1631
+ if (settled)
1632
+ return;
1633
+ settled = true;
1634
+ clearTimeout(timeout);
1635
+ const mediaEl = el;
1636
+ const d = mediaEl.duration || 0;
1637
+ URL.revokeObjectURL(url);
1638
+ resolve(d);
1639
+ };
1640
+ el.onerror = () => {
1641
+ if (settled)
1642
+ return;
1643
+ settled = true;
1644
+ clearTimeout(timeout);
1645
+ URL.revokeObjectURL(url);
1646
+ resolve(0);
1647
+ };
1648
+ el.src = url;
1649
+ }
1650
+ catch {
1651
+ resolve(0);
1652
+ }
1653
+ });
1654
+ }
1655
+ injectOverlayStyles() {
1656
+ if (document.getElementById('foisit-overlay-styles'))
1657
+ return;
1658
+ const style = document.createElement('style');
1659
+ style.id = 'foisit-overlay-styles';
1660
+ style.textContent = `
1575
1661
  :root {
1576
1662
  /* LIGHT MODE (Default) - Smoother gradient */
1577
1663
  /* Changed: Softer, right-focused radial highlight to avoid a heavy white bottom */
@@ -1893,246 +1979,299 @@ var OverlayManager = class {
1893
1979
  }
1894
1980
  .foisit-floating-btn:hover { transform: scale(1.05); }
1895
1981
  `;
1896
- document.head.appendChild(style);
1897
- }
1898
- };
1982
+ document.head.appendChild(style);
1983
+ }
1984
+ }
1899
1985
 
1900
- // dist/libs/angular-wrapper/fesm2022/foisit-angular-wrapper.mjs
1901
- var AngularWrapperComponent = class _AngularWrapperComponent {
1902
- static \u0275fac = i0.\u0275\u0275ngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: _AngularWrapperComponent, deps: [], target: i0.\u0275\u0275FactoryTarget.Component });
1903
- static \u0275cmp = i0.\u0275\u0275ngDeclareComponent({ minVersion: "14.0.0", version: "19.0.7", type: _AngularWrapperComponent, isStandalone: true, selector: "lib-angular-wrapper", ngImport: i0, template: "<p>AngularWrapper works!</p>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
1904
- };
1905
- i0.\u0275\u0275ngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AngularWrapperComponent, decorators: [{
1906
- type: Component,
1907
- args: [{ selector: "lib-angular-wrapper", imports: [CommonModule], template: "<p>AngularWrapper works!</p>\n" }]
1908
- }] });
1909
- var AssistantService = class _AssistantService {
1910
- config;
1911
- commandHandler;
1912
- fallbackHandler;
1913
- voiceProcessor;
1914
- textToSpeech;
1915
- stateManager;
1916
- lastProcessedInput = "";
1917
- processingLock = false;
1918
- isActivated = false;
1919
- // Tracks activation status
1920
- gestureHandler;
1921
- overlayManager;
1922
- defaultIntroMessage = "How can I help you?";
1923
- constructor(config) {
1924
- this.config = config;
1925
- this.commandHandler = new CommandHandler({
1926
- enableSmartIntent: this.config.enableSmartIntent !== false,
1927
- intentEndpoint: this.config.intentEndpoint
1928
- });
1929
- this.fallbackHandler = new FallbackHandler();
1930
- if (typeof window !== "undefined" && typeof document !== "undefined") {
1931
- this.voiceProcessor = new VoiceProcessor();
1932
- this.textToSpeech = new TextToSpeech();
1933
- this.gestureHandler = new GestureHandler();
1934
- this.overlayManager = new OverlayManager({
1935
- floatingButton: this.config.floatingButton,
1936
- inputPlaceholder: this.config.inputPlaceholder
1937
- });
1938
- this.overlayManager.registerCallbacks(async (input) => {
1939
- if (typeof input === "string") {
1940
- this.overlayManager.addMessage(input, "user");
1941
- } else if (input && typeof input === "object") {
1942
- const label = this.extractUserLabel(input);
1943
- if (label) {
1944
- this.overlayManager.addMessage(label, "user");
1986
+ class AssistantService {
1987
+ config;
1988
+ commandHandler;
1989
+ fallbackHandler;
1990
+ voiceProcessor;
1991
+ textToSpeech;
1992
+ stateManager;
1993
+ lastProcessedInput = '';
1994
+ processingLock = false;
1995
+ isActivated = false; // Tracks activation status
1996
+ gestureHandler;
1997
+ overlayManager;
1998
+ defaultIntroMessage = 'How can I help you?';
1999
+ constructor(config) {
2000
+ this.config = config;
2001
+ // Pass enableSmartIntent (default true) and potential apiKey (if we add it to config later)
2002
+ this.commandHandler = new CommandHandler({
2003
+ enableSmartIntent: this.config.enableSmartIntent !== false,
2004
+ intentEndpoint: this.config.intentEndpoint,
2005
+ });
2006
+ this.fallbackHandler = new FallbackHandler();
2007
+ // Browser-only features are lazily created to remain SSR-safe
2008
+ if (typeof window !== 'undefined' && typeof document !== 'undefined') {
2009
+ this.voiceProcessor = new VoiceProcessor();
2010
+ this.textToSpeech = new TextToSpeech();
2011
+ this.gestureHandler = new GestureHandler();
2012
+ this.overlayManager = new OverlayManager({
2013
+ floatingButton: this.config.floatingButton,
2014
+ inputPlaceholder: this.config.inputPlaceholder,
2015
+ });
2016
+ // Register global callbacks for floating button when overlay exists
2017
+ this.overlayManager.registerCallbacks(async (input) => {
2018
+ if (typeof input === 'string') {
2019
+ this.overlayManager.addMessage(input, 'user');
2020
+ }
2021
+ else if (input && typeof input === 'object') {
2022
+ const label = this.extractUserLabel(input);
2023
+ if (label) {
2024
+ this.overlayManager.addMessage(label, 'user');
2025
+ }
2026
+ }
2027
+ await this.handleCommand(input);
2028
+ }, () => console.log('AssistantService: Overlay closed.'));
2029
+ // Setup double-tap/double-click listener
2030
+ this.stateManager = new StateManager();
2031
+ this.gestureHandler.setupDoubleTapListener(() => this.toggle());
2032
+ }
2033
+ else {
2034
+ // Server environment: keep browser-specific properties null
2035
+ this.stateManager = undefined;
2036
+ this.voiceProcessor = undefined;
2037
+ this.textToSpeech = undefined;
2038
+ this.gestureHandler = undefined;
2039
+ this.overlayManager = undefined;
2040
+ }
2041
+ // Setup commands from config
2042
+ this.config.commands.forEach((cmd) => this.commandHandler.addCommand(cmd));
2043
+ // Setup fallback response
2044
+ if (this.config.fallbackResponse) {
2045
+ this.fallbackHandler.setFallbackMessage(this.config.fallbackResponse);
2046
+ }
2047
+ // Voice disabled for text-first pivot
2048
+ // this.startListening();
2049
+ // (moved into the browser-only block)
2050
+ }
2051
+ /** Start listening for activation and commands */
2052
+ startListening() {
2053
+ // No-op on server or when voice features are unavailable
2054
+ if (typeof window === 'undefined' || !this.voiceProcessor) {
2055
+ console.log('AssistantService: Voice is disabled or unavailable; startListening() is a no-op.');
2056
+ return;
2057
+ }
2058
+ /*
2059
+ this.voiceProcessor.startListening(async (transcript: string) => {
2060
+ if (this.processingLock) return;
2061
+
2062
+ const normalizedTranscript = transcript.toLowerCase().trim();
2063
+
2064
+ // Skip repeated or irrelevant inputs
2065
+ if (
2066
+ !normalizedTranscript ||
2067
+ normalizedTranscript.length < 3 ||
2068
+ normalizedTranscript === this.lastProcessedInput
2069
+ ) {
2070
+ console.log('AssistantService: Ignoring irrelevant input.');
2071
+ return;
2072
+ }
2073
+
2074
+ this.lastProcessedInput = normalizedTranscript;
2075
+
2076
+ if (!this.isActivated) {
2077
+ await this.processActivation(normalizedTranscript);
2078
+ return;
1945
2079
  }
2080
+
2081
+ const isFallbackOrIntroMessage =
2082
+ normalizedTranscript === this.config.fallbackResponse?.toLowerCase() ||
2083
+ normalizedTranscript === this.config.introMessage?.toLowerCase() ||
2084
+ normalizedTranscript === this.defaultIntroMessage.toLowerCase();
2085
+
2086
+ if (!isFallbackOrIntroMessage) {
2087
+ await this.handleCommand(normalizedTranscript);
2088
+ } else {
2089
+ console.log('AssistantService: Ignoring fallback or intro message.');
2090
+ }
2091
+
2092
+ // Throttle input processing to prevent rapid feedback
2093
+ this.processingLock = true;
2094
+ setTimeout(() => (this.processingLock = false), 1000);
2095
+ });
2096
+ */
2097
+ }
2098
+ /** Stop listening for voice input */
2099
+ stopListening() {
2100
+ if (typeof window === 'undefined' || !this.voiceProcessor) {
2101
+ console.log('AssistantService: Voice unavailable; stopListening() is a no-op.');
2102
+ this.isActivated = false;
2103
+ return;
2104
+ }
2105
+ this.voiceProcessor.stopListening();
2106
+ this.isActivated = false;
2107
+ }
2108
+ /** Reset activation state so the next activation flow can occur. */
2109
+ reactivate() {
2110
+ console.log('AssistantService: Reactivating assistant...');
2111
+ this.isActivated = false;
2112
+ try {
2113
+ this.startListening();
2114
+ }
2115
+ catch {
2116
+ // no-op
1946
2117
  }
1947
- await this.handleCommand(input);
1948
- }, () => console.log("AssistantService: Overlay closed."));
1949
- this.stateManager = new StateManager();
1950
- this.gestureHandler.setupDoubleTapListener(() => this.toggle());
1951
- } else {
1952
- this.stateManager = void 0;
1953
- this.voiceProcessor = void 0;
1954
- this.textToSpeech = void 0;
1955
- this.gestureHandler = void 0;
1956
- this.overlayManager = void 0;
1957
- }
1958
- this.config.commands.forEach((cmd) => this.commandHandler.addCommand(cmd));
1959
- if (this.config.fallbackResponse) {
1960
- this.fallbackHandler.setFallbackMessage(this.config.fallbackResponse);
1961
- }
1962
- }
1963
- /** Start listening for activation and commands */
1964
- startListening() {
1965
- if (typeof window === "undefined" || !this.voiceProcessor) {
1966
- console.log("AssistantService: Voice is disabled or unavailable; startListening() is a no-op.");
1967
- return;
1968
- }
1969
- }
1970
- /** Stop listening for voice input */
1971
- stopListening() {
1972
- if (typeof window === "undefined" || !this.voiceProcessor) {
1973
- console.log("AssistantService: Voice unavailable; stopListening() is a no-op.");
1974
- this.isActivated = false;
1975
- return;
1976
- }
1977
- this.voiceProcessor.stopListening();
1978
- this.isActivated = false;
1979
- }
1980
- /** Reset activation state so the next activation flow can occur. */
1981
- reactivate() {
1982
- console.log("AssistantService: Reactivating assistant...");
1983
- this.isActivated = false;
1984
- try {
1985
- this.startListening();
1986
- } catch {
1987
- }
1988
- }
1989
- /** Process activation command */
1990
- async processActivation(transcript) {
1991
- const activationCmd = this.config.activationCommand?.toLowerCase();
1992
- if (!activationCmd)
1993
- return;
1994
- if (transcript === activationCmd) {
1995
- console.log("AssistantService: Activation matched.");
1996
- this.isActivated = true;
1997
- this.textToSpeech.speak(this.config.introMessage || this.defaultIntroMessage);
1998
- } else {
1999
- console.log("AssistantService: Activation command not recognized.");
2000
- }
2001
- }
2002
- /** Handle recognized commands */
2003
- async handleCommand(input) {
2004
- this.overlayManager.showLoading();
2005
- let response;
2006
- try {
2007
- response = await this.commandHandler.executeCommand(input);
2008
- } finally {
2009
- this.overlayManager.hideLoading();
2010
- }
2011
- const originalText = typeof input === "string" ? input : void 0;
2012
- this.processResponse(response, originalText);
2013
- }
2014
- /**
2015
- * Cleanup resources
2016
- */
2017
- destroy() {
2018
- this.voiceProcessor?.stopListening();
2019
- this.gestureHandler?.destroy();
2020
- this.overlayManager?.destroy();
2021
- }
2022
- /** Unified response processing */
2023
- processResponse(response, originalText) {
2024
- if (response.type === "error" && originalText) {
2025
- this.fallbackHandler.handleFallback(originalText);
2026
- this.overlayManager.addMessage(this.fallbackHandler.getFallbackMessage(), "system");
2027
- return;
2028
- }
2029
- if (response.type === "form" && response.fields) {
2030
- this.overlayManager.addForm(response.message, response.fields, async (data) => {
2118
+ }
2119
+ /** Process activation command */
2120
+ async processActivation(transcript) {
2121
+ const activationCmd = this.config.activationCommand?.toLowerCase();
2122
+ // If no activation command is set, we can't activate via voice
2123
+ if (!activationCmd)
2124
+ return;
2125
+ if (transcript === activationCmd) {
2126
+ console.log('AssistantService: Activation matched.');
2127
+ this.isActivated = true;
2128
+ this.textToSpeech.speak(this.config.introMessage || this.defaultIntroMessage);
2129
+ // this.stateManager.setState('listening'); // DISABLED - no gradient animation
2130
+ }
2131
+ else {
2132
+ console.log('AssistantService: Activation command not recognized.');
2133
+ }
2134
+ }
2135
+ /** Handle recognized commands */
2136
+ async handleCommand(input) {
2031
2137
  this.overlayManager.showLoading();
2032
- let nextReponse;
2138
+ let response;
2033
2139
  try {
2034
- nextReponse = await this.commandHandler.executeCommand(data);
2035
- } finally {
2036
- this.overlayManager.hideLoading();
2140
+ response = await this.commandHandler.executeCommand(input);
2037
2141
  }
2038
- this.processResponse(nextReponse);
2039
- });
2040
- return;
2142
+ finally {
2143
+ this.overlayManager.hideLoading();
2144
+ }
2145
+ const originalText = typeof input === 'string' ? input : undefined;
2146
+ this.processResponse(response, originalText);
2041
2147
  }
2042
- if ((response.type === "ambiguous" || response.type === "confirm") && response.options) {
2043
- if (response.message) {
2044
- this.overlayManager.addMessage(response.message, "system");
2045
- }
2046
- this.overlayManager.addOptions(response.options);
2047
- return;
2048
- }
2049
- if (response.message) {
2050
- this.overlayManager.addMessage(response.message, "system");
2051
- }
2052
- }
2053
- /** Add a command dynamically (supports string or rich object) */
2054
- addCommand(commandOrObj, action) {
2055
- if (typeof commandOrObj === "string") {
2056
- console.log(`AssistantService: Adding command "${commandOrObj}".`);
2057
- } else {
2058
- console.log(`AssistantService: Adding rich command "${commandOrObj.command}".`);
2059
- }
2060
- this.commandHandler.addCommand(commandOrObj, action);
2061
- }
2062
- /** Remove a command dynamically */
2063
- removeCommand(command) {
2064
- console.log(`AssistantService: Removing command "${command}".`);
2065
- this.commandHandler.removeCommand(command);
2066
- }
2067
- /** Get all registered commands */
2068
- getCommands() {
2069
- return this.commandHandler.getCommands();
2070
- }
2071
- /** Toggle the assistant overlay */
2072
- toggle(onSubmit, onClose) {
2073
- console.log("AssistantService: Toggling overlay...");
2074
- this.overlayManager.toggle(async (input) => {
2075
- if (typeof input === "string") {
2076
- this.overlayManager.addMessage(input, "user");
2077
- } else if (input && typeof input === "object") {
2078
- const label = this.extractUserLabel(input);
2079
- if (label) {
2080
- this.overlayManager.addMessage(label, "user");
2148
+ /**
2149
+ * Cleanup resources
2150
+ */
2151
+ destroy() {
2152
+ this.voiceProcessor?.stopListening();
2153
+ this.gestureHandler?.destroy();
2154
+ this.overlayManager?.destroy();
2155
+ }
2156
+ /** Unified response processing */
2157
+ processResponse(response, originalText) {
2158
+ if (response.type === 'error' && originalText) {
2159
+ this.fallbackHandler.handleFallback(originalText);
2160
+ this.overlayManager.addMessage(this.fallbackHandler.getFallbackMessage(), 'system');
2161
+ return;
2081
2162
  }
2082
- }
2083
- if (onSubmit)
2084
- onSubmit(input);
2085
- await this.handleCommand(input);
2086
- }, () => {
2087
- console.log("AssistantService: Overlay closed.");
2088
- if (onClose)
2089
- onClose();
2090
- });
2091
- }
2092
- extractUserLabel(payload) {
2093
- const label = payload["label"];
2094
- if (typeof label === "string" && label.trim()) {
2095
- return label.trim();
2096
- }
2097
- const commandId = payload["commandId"];
2098
- if (typeof commandId === "string" && commandId.trim()) {
2099
- return commandId.trim();
2100
- }
2101
- return null;
2102
- }
2103
- static \u0275fac = i0.\u0275\u0275ngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: _AssistantService, deps: [{ token: "ASSISTANT_CONFIG" }], target: i0.\u0275\u0275FactoryTarget.Injectable });
2104
- static \u0275prov = i0.\u0275\u0275ngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: _AssistantService, providedIn: "root" });
2105
- };
2106
- i0.\u0275\u0275ngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantService, decorators: [{
2107
- type: Injectable,
2108
- args: [{
2109
- providedIn: "root"
2110
- }]
2111
- }], ctorParameters: () => [{ type: void 0, decorators: [{
2112
- type: Inject,
2113
- args: ["ASSISTANT_CONFIG"]
2114
- }] }] });
2115
- var AssistantModule = class _AssistantModule {
2116
- static forRoot(config) {
2117
- return {
2118
- ngModule: _AssistantModule,
2119
- providers: [
2120
- { provide: "ASSISTANT_CONFIG", useValue: config },
2121
- AssistantService
2122
- ]
2123
- };
2124
- }
2125
- static \u0275fac = i0.\u0275\u0275ngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: _AssistantModule, deps: [], target: i0.\u0275\u0275FactoryTarget.NgModule });
2126
- static \u0275mod = i0.\u0275\u0275ngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.7", ngImport: i0, type: _AssistantModule });
2127
- static \u0275inj = i0.\u0275\u0275ngDeclareInjector({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: _AssistantModule });
2128
- };
2129
- i0.\u0275\u0275ngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantModule, decorators: [{
2130
- type: NgModule,
2131
- args: [{}]
2132
- }] });
2133
- export {
2134
- AngularWrapperComponent,
2135
- AssistantModule,
2136
- AssistantService
2137
- };
2163
+ if (response.type === 'form' && response.fields) {
2164
+ this.overlayManager.addForm(response.message, response.fields, async (data) => {
2165
+ this.overlayManager.showLoading();
2166
+ let nextReponse;
2167
+ try {
2168
+ nextReponse = await this.commandHandler.executeCommand(data);
2169
+ }
2170
+ finally {
2171
+ this.overlayManager.hideLoading();
2172
+ }
2173
+ this.processResponse(nextReponse);
2174
+ });
2175
+ return;
2176
+ }
2177
+ if ((response.type === 'ambiguous' || response.type === 'confirm') && response.options) {
2178
+ if (response.message) {
2179
+ this.overlayManager.addMessage(response.message, 'system');
2180
+ }
2181
+ this.overlayManager.addOptions(response.options);
2182
+ return;
2183
+ }
2184
+ if (response.message) {
2185
+ this.overlayManager.addMessage(response.message, 'system');
2186
+ }
2187
+ }
2188
+ /** Add a command dynamically (supports string or rich object) */
2189
+ addCommand(commandOrObj, action) {
2190
+ if (typeof commandOrObj === 'string') {
2191
+ console.log(`AssistantService: Adding command "${commandOrObj}".`);
2192
+ }
2193
+ else {
2194
+ console.log(`AssistantService: Adding rich command "${commandOrObj.command}".`);
2195
+ }
2196
+ this.commandHandler.addCommand(commandOrObj, action);
2197
+ }
2198
+ /** Remove a command dynamically */
2199
+ removeCommand(command) {
2200
+ console.log(`AssistantService: Removing command "${command}".`);
2201
+ this.commandHandler.removeCommand(command);
2202
+ }
2203
+ /** Get all registered commands */
2204
+ getCommands() {
2205
+ return this.commandHandler.getCommands();
2206
+ }
2207
+ /** Toggle the assistant overlay */
2208
+ toggle(onSubmit, onClose) {
2209
+ console.log('AssistantService: Toggling overlay...');
2210
+ this.overlayManager.toggle(async (input) => {
2211
+ if (typeof input === 'string') {
2212
+ this.overlayManager.addMessage(input, 'user');
2213
+ }
2214
+ else if (input && typeof input === 'object') {
2215
+ const label = this.extractUserLabel(input);
2216
+ if (label) {
2217
+ this.overlayManager.addMessage(label, 'user');
2218
+ }
2219
+ }
2220
+ if (onSubmit)
2221
+ onSubmit(input);
2222
+ await this.handleCommand(input);
2223
+ }, () => {
2224
+ console.log('AssistantService: Overlay closed.');
2225
+ if (onClose)
2226
+ onClose();
2227
+ });
2228
+ }
2229
+ extractUserLabel(payload) {
2230
+ const label = payload['label'];
2231
+ if (typeof label === 'string' && label.trim()) {
2232
+ return label.trim();
2233
+ }
2234
+ const commandId = payload['commandId'];
2235
+ if (typeof commandId === 'string' && commandId.trim()) {
2236
+ return commandId.trim();
2237
+ }
2238
+ return null;
2239
+ }
2240
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantService, deps: [{ token: 'ASSISTANT_CONFIG' }], target: i0.ɵɵFactoryTarget.Injectable });
2241
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantService, providedIn: 'root' });
2242
+ }
2243
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantService, decorators: [{
2244
+ type: Injectable,
2245
+ args: [{
2246
+ providedIn: 'root',
2247
+ }]
2248
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
2249
+ type: Inject,
2250
+ args: ['ASSISTANT_CONFIG']
2251
+ }] }] });
2252
+
2253
+ class AssistantModule {
2254
+ static forRoot(config) {
2255
+ return {
2256
+ ngModule: AssistantModule,
2257
+ providers: [
2258
+ { provide: 'ASSISTANT_CONFIG', useValue: config },
2259
+ AssistantService,
2260
+ ],
2261
+ };
2262
+ }
2263
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
2264
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.7", ngImport: i0, type: AssistantModule });
2265
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantModule });
2266
+ }
2267
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AssistantModule, decorators: [{
2268
+ type: NgModule,
2269
+ args: [{}]
2270
+ }] });
2271
+
2272
+ /**
2273
+ * Generated bundle index. Do not edit.
2274
+ */
2275
+
2276
+ export { AngularWrapperComponent, AssistantModule, AssistantService };
2138
2277
  //# sourceMappingURL=foisit-angular-wrapper.mjs.map