reneco-advanced-input-module 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/.editorconfig +15 -0
  2. package/.prettierrc.json +13 -0
  3. package/LICENSE +21 -0
  4. package/api-key-inject.js +46 -0
  5. package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
  6. package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
  7. package/dist/cjs/file-uploader.voice-input-module.entry.cjs.js.map +1 -0
  8. package/dist/cjs/file-uploader_2.cjs.entry.js +1319 -0
  9. package/dist/cjs/file-uploader_2.cjs.entry.js.map +1 -0
  10. package/dist/cjs/index-BTSzTkSZ.js +1494 -0
  11. package/dist/cjs/index-BTSzTkSZ.js.map +1 -0
  12. package/dist/cjs/index.cjs.js +5 -0
  13. package/dist/cjs/index.cjs.js.map +1 -0
  14. package/dist/cjs/loader.cjs.js +16 -0
  15. package/dist/cjs/loader.cjs.js.map +1 -0
  16. package/dist/cjs/voice-input-module.cjs.js +28 -0
  17. package/dist/cjs/voice-input-module.cjs.js.map +1 -0
  18. package/dist/collection/collection-manifest.json +13 -0
  19. package/dist/collection/components/file-uploader/file-uploader.css +26 -0
  20. package/dist/collection/components/file-uploader/file-uploader.js +130 -0
  21. package/dist/collection/components/file-uploader/file-uploader.js.map +1 -0
  22. package/dist/collection/components/voice-input-module/voice-input-module.css +251 -0
  23. package/dist/collection/components/voice-input-module/voice-input-module.js +875 -0
  24. package/dist/collection/components/voice-input-module/voice-input-module.js.map +1 -0
  25. package/dist/collection/index.js +12 -0
  26. package/dist/collection/index.js.map +1 -0
  27. package/dist/collection/services/audio-recorder.service.js +66 -0
  28. package/dist/collection/services/audio-recorder.service.js.map +1 -0
  29. package/dist/collection/services/llm.service.js +193 -0
  30. package/dist/collection/services/llm.service.js.map +1 -0
  31. package/dist/collection/services/speech-to-text.service.js +62 -0
  32. package/dist/collection/services/speech-to-text.service.js.map +1 -0
  33. package/dist/collection/types/form-schema.types.js +2 -0
  34. package/dist/collection/types/form-schema.types.js.map +1 -0
  35. package/dist/collection/types/service-providers.types.js +2 -0
  36. package/dist/collection/types/service-providers.types.js.map +1 -0
  37. package/dist/collection/utils/schema-converter.js +422 -0
  38. package/dist/collection/utils/schema-converter.js.map +1 -0
  39. package/dist/components/file-uploader.d.ts +11 -0
  40. package/dist/components/file-uploader.js +9 -0
  41. package/dist/components/file-uploader.js.map +1 -0
  42. package/dist/components/file-uploader2.js +98 -0
  43. package/dist/components/file-uploader2.js.map +1 -0
  44. package/dist/components/index.d.ts +33 -0
  45. package/dist/components/index.js +4 -0
  46. package/dist/components/index.js.map +1 -0
  47. package/dist/components/voice-input-module.d.ts +11 -0
  48. package/dist/components/voice-input-module.js +1292 -0
  49. package/dist/components/voice-input-module.js.map +1 -0
  50. package/dist/esm/app-globals-DQuL1Twl.js +6 -0
  51. package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
  52. package/dist/esm/file-uploader.voice-input-module.entry.js.map +1 -0
  53. package/dist/esm/file-uploader_2.entry.js +1316 -0
  54. package/dist/esm/file-uploader_2.entry.js.map +1 -0
  55. package/dist/esm/index-jmc2yzBp.js +1487 -0
  56. package/dist/esm/index-jmc2yzBp.js.map +1 -0
  57. package/dist/esm/index.js +4 -0
  58. package/dist/esm/index.js.map +1 -0
  59. package/dist/esm/loader.js +14 -0
  60. package/dist/esm/loader.js.map +1 -0
  61. package/dist/esm/voice-input-module.js +24 -0
  62. package/dist/esm/voice-input-module.js.map +1 -0
  63. package/dist/index.cjs.js +1 -0
  64. package/dist/index.js +1 -0
  65. package/dist/types/components/file-uploader/file-uploader.d.ts +8 -0
  66. package/dist/types/components/voice-input-module/voice-input-module.d.ts +55 -0
  67. package/dist/types/components.d.ts +158 -0
  68. package/dist/types/index.d.ts +9 -0
  69. package/dist/types/services/audio-recorder.service.d.ts +9 -0
  70. package/dist/types/services/llm.service.d.ts +15 -0
  71. package/dist/types/services/speech-to-text.service.d.ts +11 -0
  72. package/dist/types/stencil-public-runtime.d.ts +1709 -0
  73. package/dist/types/types/form-schema.types.d.ts +70 -0
  74. package/dist/types/types/service-providers.types.d.ts +20 -0
  75. package/dist/types/utils/schema-converter.d.ts +22 -0
  76. package/dist/voice-input-module/file-uploader.voice-input-module.entry.esm.js.map +1 -0
  77. package/dist/voice-input-module/index.esm.js +2 -0
  78. package/dist/voice-input-module/index.esm.js.map +1 -0
  79. package/dist/voice-input-module/loader.esm.js.map +1 -0
  80. package/dist/voice-input-module/p-7b4f33ba.entry.js +2 -0
  81. package/dist/voice-input-module/p-7b4f33ba.entry.js.map +1 -0
  82. package/dist/voice-input-module/p-DQuL1Twl.js +2 -0
  83. package/dist/voice-input-module/p-DQuL1Twl.js.map +1 -0
  84. package/dist/voice-input-module/p-jmc2yzBp.js +3 -0
  85. package/dist/voice-input-module/p-jmc2yzBp.js.map +1 -0
  86. package/dist/voice-input-module/voice-input-module.esm.js +2 -0
  87. package/dist/voice-input-module/voice-input-module.esm.js.map +1 -0
  88. package/env-config.js +4 -0
  89. package/inject-env.js +20 -0
  90. package/package.json +37 -0
  91. package/readme.md +111 -0
  92. package/src/components/file-uploader/file-uploader.css +26 -0
  93. package/src/components/file-uploader/file-uploader.tsx +100 -0
  94. package/src/components/file-uploader/readme.md +31 -0
  95. package/src/components/voice-input-module/readme.md +114 -0
  96. package/src/components/voice-input-module/voice-input-module.css +251 -0
  97. package/src/components/voice-input-module/voice-input-module.tsx +731 -0
  98. package/src/components.d.ts +158 -0
  99. package/src/index.html +663 -0
  100. package/src/index.ts +12 -0
  101. package/src/services/audio-recorder.service.ts +74 -0
  102. package/src/services/llm.service.ts +221 -0
  103. package/src/services/speech-to-text.service.ts +72 -0
  104. package/src/types/form-schema.types.ts +78 -0
  105. package/src/types/service-providers.types.ts +22 -0
  106. package/src/utils/schema-converter.ts +494 -0
  107. package/stencil.config.ts +24 -0
  108. package/tsconfig.json +30 -0
@@ -0,0 +1,731 @@
1
+ import { Component, Prop, Event, EventEmitter, State, h, Watch, Method } from '@stencil/core';
2
+ import { FormSchema, VoiceFormRecorderResult, FilledFormData, FormSchemaFieldsOnly, FormSchemaFieldsOnlyExtended } from '../../types/form-schema.types';
3
+ import { ServiceProviderConfig } from '../../types/service-providers.types';
4
+ import { AudioRecorderService } from '../../services/audio-recorder.service';
5
+ import { SpeechToTextServiceFactory } from '../../services/speech-to-text.service';
6
+ import { LLMServiceFactory } from '../../services/llm.service';
7
+ import { SchemaConverter } from '../../utils/schema-converter';
8
+
9
+ @Component({
10
+ tag: 'voice-input-module',
11
+ styleUrl: 'voice-input-module.css',
12
+ shadow: true,
13
+ })
14
+ export class VoiceFormRecorder {
15
+ @Prop() formJson: string = '{}';
16
+ @Prop() serviceConfig: string = '{}';
17
+ @Prop() apiKey: string;
18
+ @Prop() context: 'track'|'ng'|'ecoll-veto'|'ecoteka' = undefined;
19
+ @Prop() classificationRootUrl: string = 'http://localhost';
20
+ @Prop() language: 'fr'|'en' = 'en';
21
+ @Prop() voiceOrOcr: 'voice'|'ocr'|'both' = undefined;
22
+
23
+ @Prop() debug: boolean = false;
24
+ @Prop() renderForm: boolean = false;
25
+ @Prop() displayStatus: boolean = false;
26
+
27
+ @Event() formFilled: EventEmitter<VoiceFormRecorderResult>;
28
+ @Event() recordingStateChanged: EventEmitter<{ isRecording: boolean; state: string }>;
29
+
30
+ @State() isRecording: boolean = false;
31
+ @State() isProcessing: boolean = false;
32
+ @State() statusMessage: string;
33
+ @State() hasError: boolean = false;
34
+ @State() transcription: string = '';
35
+ @State() filledData: FilledFormData | null = null;
36
+ @State() debugInfo: any = {};
37
+ @State() isReadonlyMode: boolean = true; // Start in readonly preview mode
38
+
39
+ private audioRecorder: AudioRecorderService;
40
+ private speechToTextService: any;
41
+ private llmService: any;
42
+ private parsedSchema: any; // Support both simple FormSchema and complex nested schema
43
+ private parsedConfig: ServiceProviderConfig;
44
+
45
+ constructor() {
46
+ this.audioRecorder = new AudioRecorderService();
47
+ }
48
+
49
+ componentWillLoad() {
50
+ this.initializeServices();
51
+ }
52
+
53
+ @Watch('formJson')
54
+ @Watch('serviceConfig')
55
+ initializeServices() {
56
+ try {
57
+ if (!this.context){
58
+ this.hasError = true;
59
+ const errorMessage = (this.language == 'en' ? `Initialization error: context is '${this.context}'` : `Erreur d'initialisation: le contexte est '${this.context}'`);
60
+ this.statusMessage = errorMessage;
61
+ this.updateDebugInfo(errorMessage, { error: errorMessage });
62
+ }
63
+ else{
64
+ // Parse form schema
65
+ this.parsedSchema = JSON.parse(this.formJson || '{}');
66
+
67
+ // Parse service configuration
68
+ this.parsedConfig = JSON.parse(this.serviceConfig || '{}');
69
+
70
+ // Add API key to config if provided via prop
71
+ if (this.apiKey) {
72
+ this.parsedConfig = {
73
+ ...this.parsedConfig,
74
+ speechToText: { ...this.parsedConfig.speechToText, apiKey: this.apiKey },
75
+ llm: { ...this.parsedConfig.llm, apiKey: this.apiKey }
76
+ };
77
+ }
78
+
79
+ // Initialize services
80
+ this.speechToTextService = SpeechToTextServiceFactory.create(this.parsedConfig);
81
+ this.llmService = LLMServiceFactory.create(this.parsedConfig);
82
+
83
+ this.updateDebugInfo('Initialized', {
84
+ schema: this.parsedSchema,
85
+ config: this.parsedConfig
86
+ });
87
+
88
+ this.hasError = false;
89
+ this.statusMessage = (this.language == 'en' ? 'Click to start recording' : 'Cliquer pour enregistrer');
90
+ }
91
+ } catch (error) {
92
+ this.hasError = true;
93
+ this.statusMessage = (this.language == 'en' ? `Initialization error: ${error.message}` : `Erreur d'initialisation: ${error.message}`);
94
+ this.updateDebugInfo('Initialization Error', { error: error.message });
95
+ }
96
+ }
97
+
98
+ private updateDebugInfo(action: string, data: any) {
99
+ if (this.debug) {
100
+ this.debugInfo = {
101
+ ...this.debugInfo,
102
+ [action]: {
103
+ timestamp: new Date().toISOString(),
104
+ data
105
+ }
106
+ };
107
+ }
108
+ }
109
+
110
+ private async handleRecordClick() {
111
+ if (this.isProcessing) return;
112
+
113
+ if (this.isRecording) {
114
+ await this.stopRecordingAndProcess();
115
+ } else {
116
+ await this.startRecording();
117
+ }
118
+ }
119
+
120
+ private async startRecording() {
121
+ try {
122
+ this.hasError = false;
123
+ this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
124
+ this.updateDebugInfo('Start Recording Attempt', {});
125
+
126
+ await this.audioRecorder.startRecording();
127
+
128
+ this.isRecording = true;
129
+ this.statusMessage = (this.language == 'en' ? 'Recording... Click to stop' : 'Enregistrement ... Cliquer pour stopper');
130
+ this.updateDebugInfo('Recording Started', {});
131
+
132
+ this.recordingStateChanged.emit({
133
+ isRecording: true,
134
+ state: 'recording'
135
+ });
136
+ } catch (error) {
137
+ this.hasError = true;
138
+ this.statusMessage = (this.language == 'en' ? `Recording failed: ${error.message}` : `Echec de l'enregistrement : ${error.message}`);
139
+ this.updateDebugInfo('Recording Error', { error: error.message });
140
+
141
+ this.formFilled.emit({
142
+ success: false,
143
+ error: error.message
144
+ });
145
+ }
146
+ }
147
+
148
+ private async processJsonForm(jsonForm: string){
149
+ // console.log("processJsonForm", jsonForm);
150
+ try {
151
+ this.isProcessing = true;
152
+ this.statusMessage = (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
153
+
154
+ // Fill form using LLM
155
+ this.statusMessage = (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
156
+ const trimmedSchema = this.trimSchemaForAI(this.parsedSchema);
157
+ const filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
158
+
159
+ // Extract filled data
160
+ this.filledData = this.extractFilledData(filledSchema);
161
+ this.updateDebugInfo('Form Filled', {
162
+ filledSchema,
163
+ extractedData: this.filledData
164
+ });
165
+
166
+ this.parsedSchema = this.filledData;
167
+
168
+ this.statusMessage = (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
169
+ this.hasError = false;
170
+
171
+ // Emit success event
172
+ this.formFilled.emit({
173
+ success: true,
174
+ data: this.filledData,
175
+ jsonForm: jsonForm
176
+ });
177
+
178
+ } catch (error) {
179
+ this.hasError = true;
180
+ this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
181
+
182
+ this.formFilled.emit({
183
+ success: false,
184
+ error: error.message,
185
+ jsonForm: jsonForm
186
+ });
187
+ } finally {
188
+ this.isProcessing = false;
189
+ }
190
+ }
191
+
192
+ private async stopRecordingAndProcess() {
193
+ try {
194
+ this.isRecording = false;
195
+ this.isProcessing = true;
196
+ this.statusMessage = (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
197
+ this.updateDebugInfo('Stop Recording', {});
198
+
199
+ this.recordingStateChanged.emit({
200
+ isRecording: false,
201
+ state: 'processing'
202
+ });
203
+
204
+ // Stop recording and get audio blob
205
+ const audioBlob = await this.audioRecorder.stopRecording();
206
+ this.updateDebugInfo('Audio Captured', {
207
+ size: audioBlob.size,
208
+ type: audioBlob.type
209
+ });
210
+
211
+ // Transcribe audio
212
+ this.statusMessage = (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
213
+ const transcription = await this.speechToTextService.transcribe(audioBlob, this.language);
214
+ this.transcription = transcription;
215
+ this.updateDebugInfo('Transcription Complete', { transcription });
216
+
217
+ if (!transcription.trim()) {
218
+ throw new Error('No speech detected in the recording');
219
+ }
220
+
221
+ // Fill form using LLM
222
+ this.statusMessage = (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
223
+ const trimmedSchema = this.trimSchemaForAI(this.parsedSchema);
224
+ const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
225
+
226
+ // Extract filled data
227
+ this.filledData = this.extractFilledData(filledSchema);
228
+ this.updateDebugInfo('Form Filled', {
229
+ filledSchema,
230
+ extractedData: this.filledData
231
+ });
232
+
233
+ this.parsedSchema = this.filledData;
234
+
235
+ this.statusMessage = (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
236
+ this.hasError = false;
237
+
238
+ // Emit success event
239
+ this.formFilled.emit({
240
+ success: true,
241
+ data: this.filledData,
242
+ transcription: transcription
243
+ });
244
+
245
+ } catch (error) {
246
+ this.hasError = true;
247
+ this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
248
+ this.updateDebugInfo('Processing Error', { error: error.message });
249
+
250
+ this.formFilled.emit({
251
+ success: false,
252
+ error: error.message,
253
+ transcription: this.transcription
254
+ });
255
+ } finally {
256
+ this.isProcessing = false;
257
+ this.recordingStateChanged.emit({
258
+ isRecording: false,
259
+ state: 'idle'
260
+ });
261
+ }
262
+ }
263
+
264
+ private extractFilledData(filledData: any): any {
265
+ // console.log("extractFilledData", filledData);
266
+ const updatedSchema = JSON.parse(JSON.stringify(this.parsedSchema));
267
+ switch(this.context){
268
+ case "ng":
269
+ if (filledData?.fields) {
270
+ // Map AI response back to original schema structure
271
+ filledData.fields.forEach((field: any) => {
272
+ const originalField = updatedSchema.Children.find((child: any) =>
273
+ child.System_Name === field.name
274
+ );
275
+
276
+ if (originalField && field.value !== undefined && field.value !== null && field.value !== '') {
277
+ if (!originalField.Settings) originalField.Settings = {};
278
+ originalField.Settings.Default_Value = field.value;
279
+ }
280
+ });
281
+ }
282
+
283
+ break;
284
+ case "ecoll-veto":
285
+ // console.log("TODO extractFilledData", filledData, updatedSchema);
286
+ if (filledData?.fields) {
287
+ // Map AI response back to original schema structure
288
+ filledData.fields.forEach((field: any) => {
289
+ let originalField = updatedSchema[0].items.find((child: any) =>
290
+ child.label === field.name
291
+ );
292
+ if (!originalField)
293
+ originalField = updatedSchema[1].items.find((child: any) =>
294
+ child.label === field.name
295
+ );
296
+
297
+ if (originalField && field.value !== undefined && field.value !== null && field.value !== '') {
298
+ updatedSchema[2][originalField.name] = field.value;
299
+ }
300
+ });
301
+ }
302
+ break;
303
+ case "ecoteka":
304
+ // console.log("TODO extractFilledData", filledData);
305
+ break;
306
+ case "track":
307
+ default:
308
+ const data: FilledFormData = {};
309
+
310
+ Object.entries(filledData.fields).forEach(([fieldID, field]: [string, any]) => {
311
+ if (field.default !== undefined && field.default !== null && field.default !== '') {
312
+ updatedSchema[fieldID] = field.default;
313
+ }
314
+ if (field.value !== undefined && field.value !== null && field.value !== '') {
315
+ for (const key in updatedSchema.fields) {
316
+ const schemaField = updatedSchema.fields[key];
317
+ if (schemaField.title === field.name) {
318
+ schemaField.value = field.value || field.default;
319
+ schemaField.default = field.value || field.default;
320
+ break; // stop after finding the first match
321
+ }
322
+ }
323
+ }
324
+ });
325
+
326
+ break;
327
+ }
328
+
329
+ // console.log("extractFilledData result", updatedSchema);
330
+ return updatedSchema;
331
+ }
332
+
333
+ private trimSchemaForAI(schema: any): any {
334
+ // console.log("trimSchemaForAI", schema);
335
+ switch(this.context){
336
+ case 'ng':
337
+ const trimmed = { fields: [] };
338
+ schema.Children.forEach((child: any) => {
339
+ if (!child.System_Name || !child.Type) return;
340
+
341
+ const fieldData: any = {
342
+ name: child.System_Name,
343
+ type: this.mapFieldType(child.Type)
344
+ };
345
+
346
+ // Add options for classification/select fields
347
+ if (child.Type === 'InputClassification' && child.Children && child.Children.length > 0) {
348
+ fieldData.options = child.Children.map((option: any) =>
349
+ option.System_Name || option.Label || option.toString()
350
+ );
351
+ }
352
+
353
+ trimmed.fields.push(fieldData);
354
+ });
355
+
356
+ // console.log("Schema apres transformation, contexte NG:", trimmed);
357
+ return trimmed;
358
+ case 'ecoll-veto':
359
+ // console.log("TODO trimSchemaForAI", schema)
360
+ const mergedItemsSchema = (this.parsedSchema[0].items).concat(this.parsedSchema[1].items);
361
+
362
+ if (mergedItemsSchema) {
363
+ const trimmedSchema: FormSchema = {
364
+ title: 'Form Name',
365
+ description: 'Form Description',
366
+ schema: {}
367
+ };
368
+
369
+ Object.entries(mergedItemsSchema).forEach(([key, field]: [string, any]) => {
370
+ const fieldName = field.name;
371
+ const fieldType = this.mapFieldType(field.type) as any;
372
+ const fieldLabel = field.label || fieldName;
373
+ const isRequired = field.required || false;
374
+ const fieldValue = ''; //TODO
375
+
376
+ trimmedSchema.schema[fieldName] = {
377
+ type: fieldType,
378
+ title: fieldLabel,
379
+ options: field.options,
380
+ readonly: field.readonly === true,
381
+ default: '',
382
+ };
383
+ });
384
+
385
+ // console.log("Schema apres transformation, contexte Track:", trimmedSchema);
386
+ return trimmedSchema;
387
+ }
388
+ case "ecoteka":
389
+ // console.log("TODO trimSchemaForAI", schema)
390
+ break;
391
+ case 'track':
392
+ default:
393
+ // Handle simple schema format (backward compatibility)
394
+ if (schema?.schema ?? schema?.fields) {
395
+ const trimmedSchema: FormSchema = {
396
+ title: schema.title,
397
+ description: schema.description,
398
+ schema: {}
399
+ };
400
+
401
+ const finalSchema = schema?.schema ?? schema?.fields;
402
+
403
+ Object.entries(finalSchema).forEach(([fieldName, field]: [string, any]) => {
404
+ trimmedSchema.schema[fieldName] = {
405
+ type: field.type,
406
+ title: field.title,
407
+ options: field.options,
408
+ readonly: field.Enabled === false,
409
+ default: field.DefaultValue,
410
+ pattern: field.Mask,
411
+ min: field.ValidationMin,
412
+ max: field.ValidationMax
413
+ };
414
+ });
415
+
416
+
417
+ // console.log("Schema apres transformation, contexte Track:", trimmedSchema);
418
+ return trimmedSchema;
419
+ }
420
+ break;
421
+ }
422
+
423
+ return schema;
424
+ }
425
+
426
+ private mapFieldType(type: string): string {
427
+ const typeMapping = {
428
+ 'InputAutocomplete': 'string',
429
+ 'InputInteger': 'number',
430
+ 'InputTextArea': 'string',
431
+ 'InputDateTimePicker': 'datetime',
432
+ 'InputDecimal': 'number',
433
+ 'InputClassification': 'select',
434
+ 'InputCheckbox': 'boolean',
435
+ 'InputTextTranslation': 'string',
436
+ 'thesaurus': 'string',
437
+ 'position': 'string',
438
+ 'text': 'string',
439
+ 'textarea': 'string',
440
+ 'number': 'number',
441
+ 'date': 'date',
442
+ 'datetime': 'datetime'
443
+ };
444
+
445
+ return typeMapping[type] || 'string';
446
+ }
447
+
448
+ // Utility methods exposed as public API
449
+ @Method() public async convertXmlToJson(xmlForm: string): Promise<FormSchemaFieldsOnly> {
450
+ return SchemaConverter.convertXmlToJson(xmlForm, this.classificationRootUrl, this.language);
451
+ }
452
+
453
+ @Method() public async convertJsonToXml(jsonForm: FormSchemaFieldsOnlyExtended): Promise<string> {
454
+ return SchemaConverter.convertJsonToXml(jsonForm);
455
+ }
456
+
457
+ // Utility methods exposed as public API
458
+ @Method() public async convertXmlToJsonLegacy(xmlForm: string): Promise<FormSchema> {
459
+ return SchemaConverter.convertXmlToJsonLegacy(xmlForm);
460
+ }
461
+
462
+ @Method() public async convertJsonToXmlLegacy(jsonForm: FormSchema): Promise<string> {
463
+ return SchemaConverter.convertJsonToXmlLegacy(jsonForm);
464
+ }
465
+
466
+ private renderUploadButton() {
467
+ if (!['ocr', 'both'].includes(this.voiceOrOcr))
468
+ return;
469
+ return(
470
+ <file-uploader
471
+ batch={false}
472
+ callback={(data) => {this.processJsonForm(data);}}
473
+ >
474
+ </file-uploader>
475
+ );
476
+ }
477
+
478
+ private renderRecordButton() {
479
+ if (!['voice', 'both', undefined].includes(this.voiceOrOcr))
480
+ return;
481
+
482
+ const buttonClass = [
483
+ 'record-button',
484
+ this.isRecording && 'recording',
485
+ this.isProcessing && 'processing'
486
+ ].filter(Boolean).join(' ');
487
+
488
+ const isDisabled = this.isProcessing || this.hasError;
489
+
490
+ return (
491
+ <button
492
+ class={buttonClass}
493
+ onClick={() => this.handleRecordClick()}
494
+ disabled={isDisabled}
495
+ aria-label={this.isRecording ? 'Stop recording' : 'Start recording'}
496
+ >
497
+ {this.isProcessing ? (
498
+ <svg class="record-icon" viewBox="0 0 24 24">
499
+ <circle cx="12" cy="12" r="3">
500
+ <animate attributeName="r" values="3;6;3" dur="1s" repeatCount="indefinite" />
501
+ <animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite" />
502
+ </circle>
503
+ </svg>
504
+ ) : this.isRecording ? (
505
+ <svg class="record-icon" viewBox="0 0 24 24">
506
+ <rect x="6" y="6" width="12" height="12" rx="2" />
507
+ </svg>
508
+ ) : (
509
+ <svg class="record-icon" viewBox="0 0 24 24">
510
+ <circle cx="12" cy="12" r="8" />
511
+ </svg>
512
+ )}
513
+ </button>
514
+ );
515
+ }
516
+
517
+ private renderStatusMessage() {
518
+ const statusClass = [
519
+ 'status-text',
520
+ this.hasError && 'error',
521
+ this.filledData && !this.hasError && 'success'
522
+ ].filter(Boolean).join(' ');
523
+
524
+ return <div class={statusClass}>{this.statusMessage}</div>;
525
+ }
526
+
527
+ private renderFormPreview() {
528
+ if (!this.parsedSchema) return null;
529
+
530
+ const isPreview = this.isReadonlyMode && !this.filledData;
531
+ const title = isPreview ? 'Form Preview (Voice input to fill)' : 'Voice-Filled Form:';
532
+
533
+ return (
534
+ <div class="form-preview">
535
+ <div class="form-preview-title">{title}</div>
536
+ <form class="voice-filled-form">
537
+ {this.renderFormFields()}
538
+ </form>
539
+ </div>
540
+ );
541
+ }
542
+
543
+ private renderFormFields() {
544
+ // console.log("renderFormFields", this.parsedSchema);
545
+ if (!this.parsedSchema) return null;
546
+
547
+ switch(this.context){
548
+ case "ng":
549
+ return this.parsedSchema.Children.map((child: any) => {
550
+ if (!child.System_Name || !child.Type) return null;
551
+
552
+ const fieldName = child.System_Name;
553
+ const fieldType = this.mapFieldType(child.Type);
554
+ const fieldLabel = child.Settings?.Label || child.System_Name;
555
+ const isRequired = child.Required || false;
556
+ const fieldValue = child.Settings?.Default_Value;
557
+
558
+ return (
559
+ <div class="form-group" key={fieldName}>
560
+ <label htmlFor={fieldName} class="form-label">
561
+ {fieldLabel}
562
+ {isRequired && <span class="required">*</span>}
563
+ </label>
564
+ {this.renderFormField(fieldName, {
565
+ type: fieldType,
566
+ title: fieldLabel,
567
+ required: isRequired,
568
+ options: child.Children?.map((option: any) =>
569
+ option.System_Name || option.Label || option.toString()
570
+ )
571
+ }, fieldValue)}
572
+ </div>
573
+ );
574
+ }).filter(Boolean);
575
+ case "ecoll-veto":
576
+ // NOTE STEP 2
577
+ // console.log("TODO renderFormFields", this.parsedSchema);
578
+ const mergedItemsSchema = (this.parsedSchema[0].items).concat(this.parsedSchema[1].items);
579
+ return Object.entries(mergedItemsSchema).map(([key, field]: [string, any]) => {
580
+
581
+ const fieldName = field.name;
582
+ field.type = this.mapFieldType(field.type);
583
+ const fieldLabel = field.label || fieldName;
584
+ const isRequired = field.required || false;
585
+ const fieldValue = this.parsedSchema[2][fieldName];
586
+
587
+ return (
588
+ <div class="form-group" key={fieldName}>
589
+ <label htmlFor={fieldName} class="form-label">
590
+ { fieldLabel }
591
+ {isRequired && <span class="required">*</span>}
592
+ </label>
593
+ {this.renderFormField(fieldName, field, (this.filledData?.[fieldName] ?? fieldValue))}
594
+ </div>
595
+ );
596
+ });
597
+ case "ecoteka":
598
+ // console.log("TODO renderFormFields", this.parsedSchema);
599
+ break;
600
+ case "track":
601
+ default:
602
+ return Object.entries(this.parsedSchema.fields).map(([fieldName, field]: [string, any]) => (
603
+ <div class="form-group" key={fieldName}>
604
+ <label htmlFor={fieldName} class="form-label">
605
+ {field.title || fieldName}
606
+ {field.required && <span class="required">*</span>}
607
+ </label>
608
+ {this.renderFormField(fieldName, field, (this.filledData?.[fieldName] ?? field.value))}
609
+ </div>
610
+ ));
611
+ }
612
+
613
+ return null;
614
+ }
615
+
616
+ private renderFormField(fieldName: string, field: any, value: any) {
617
+ const isReadonly = this.isReadonlyMode && !this.filledData;
618
+ const commonProps = {
619
+ id: fieldName,
620
+ name: fieldName,
621
+ class: 'form-input',
622
+ required: field.required,
623
+ disabled: isReadonly
624
+ };
625
+
626
+ switch (field.type) {
627
+ case 'select':
628
+ if (isReadonly) {
629
+ // In readonly mode, show all options as a list instead of dropdown
630
+ return (
631
+ <div class="readonly-select">
632
+ <div class="select-placeholder">Available options:</div>
633
+ <ul class="select-options-list">
634
+ {field.options?.map(option => (
635
+ <li class="select-option">{option}</li>
636
+ ))}
637
+ </ul>
638
+ </div>
639
+ );
640
+ }
641
+ return (
642
+ <select id={fieldName} name={fieldName} class="form-input" required={field.required}>
643
+ <option value="">-- Select --</option>
644
+ {field.options?.map(option => (
645
+ <option value={option} selected={value === option}>
646
+ {option}
647
+ </option>
648
+ ))}
649
+ </select>
650
+ );
651
+
652
+ case 'boolean':
653
+ return (
654
+ <input
655
+ {...commonProps}
656
+ type="checkbox"
657
+ class="form-checkbox"
658
+ checked={value === true || value === 'true'}
659
+ />
660
+ );
661
+
662
+ case 'number':
663
+ return (
664
+ <input
665
+ {...commonProps}
666
+ type="number"
667
+ min={field.min}
668
+ max={field.max}
669
+ step="any"
670
+ value={value || ''}
671
+ />
672
+ );
673
+
674
+ case 'date':
675
+ return (
676
+ <input
677
+ {...commonProps}
678
+ type='date'
679
+ value={value ? (([d, m, y]) => `${y}-${m}-${d}`)(value.split("/")) : ''}
680
+ />
681
+ );
682
+
683
+ case 'datetime':
684
+ return (
685
+ <input
686
+ {...commonProps}
687
+ type='datetime-local'
688
+ value={value || ''}
689
+ />
690
+ );
691
+
692
+ default: // string
693
+ return (
694
+ <input
695
+ {...commonProps}
696
+ type="text"
697
+ pattern={field.pattern}
698
+ placeholder={field.description}
699
+ value={value || ''}
700
+ />
701
+ );
702
+ }
703
+ }
704
+
705
+ private renderDebugPanel() {
706
+ if (!this.debug) return null;
707
+
708
+ return (
709
+ <div class="debug-panel">
710
+ <div class="debug-title">Debug Information:</div>
711
+ <div class="debug-content">
712
+ {JSON.stringify(this.debugInfo, null, 2)}
713
+ </div>
714
+ </div>
715
+ );
716
+ }
717
+
718
+ render() {
719
+ return (
720
+ <div>
721
+ <div class={"voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : "")}>
722
+ {this.renderRecordButton()}
723
+ {this.displayStatus ? this.renderStatusMessage() : ""}
724
+ {this.renderUploadButton()}
725
+ {this.renderForm ? this.renderFormPreview() : ""}
726
+ {this.debug ? this.renderDebugPanel() : ""}
727
+ </div>
728
+ </div>
729
+ );
730
+ }
731
+ }