reneco-advanced-input-module 0.0.1-beta.1 → 0.0.1-beta.2

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 (43) hide show
  1. package/loader/cdn.js +1 -0
  2. package/loader/index.cjs.js +1 -0
  3. package/loader/index.d.ts +24 -0
  4. package/loader/index.es2017.js +1 -0
  5. package/loader/index.js +2 -0
  6. package/package.json +6 -2
  7. package/www/build/index.esm.js +2 -0
  8. package/www/build/index.esm.js.map +1 -0
  9. package/www/build/loader.esm.js.map +1 -0
  10. package/www/build/ocr-file-uploader.voice-input-module.entry.esm.js.map +1 -0
  11. package/www/build/p-52e59129.entry.js +2 -0
  12. package/www/build/p-52e59129.entry.js.map +1 -0
  13. package/www/build/p-DQuL1Twl.js +2 -0
  14. package/www/build/p-DQuL1Twl.js.map +1 -0
  15. package/www/build/p-jmc2yzBp.js +3 -0
  16. package/www/build/p-jmc2yzBp.js.map +1 -0
  17. package/www/build/voice-input-module.esm.js +2 -0
  18. package/www/build/voice-input-module.esm.js.map +1 -0
  19. package/www/build/voice-input-module.js +33 -0
  20. package/www/host.config.json +15 -0
  21. package/www/index.html +922 -0
  22. package/.editorconfig +0 -15
  23. package/.prettierrc.json +0 -13
  24. package/api-key-inject.js +0 -46
  25. package/env-config.js +0 -4
  26. package/inject-env.js +0 -20
  27. package/src/components/ocr-file-uploader/ocr-file-uploader.css +0 -26
  28. package/src/components/ocr-file-uploader/ocr-file-uploader.tsx +0 -100
  29. package/src/components/ocr-file-uploader/readme.md +0 -31
  30. package/src/components/voice-input-module/readme.md +0 -114
  31. package/src/components/voice-input-module/voice-input-module.css +0 -286
  32. package/src/components/voice-input-module/voice-input-module.tsx +0 -778
  33. package/src/components.d.ts +0 -158
  34. package/src/index.html +0 -1015
  35. package/src/index.ts +0 -12
  36. package/src/services/audio-recorder.service.ts +0 -74
  37. package/src/services/llm.service.ts +0 -221
  38. package/src/services/speech-to-text.service.ts +0 -70
  39. package/src/types/form-schema.types.ts +0 -78
  40. package/src/types/service-providers.types.ts +0 -22
  41. package/src/utils/schema-converter.ts +0 -494
  42. package/stencil.config.ts +0 -24
  43. package/tsconfig.json +0 -30
@@ -1,778 +0,0 @@
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() inputTypes: ('voice' | 'ocr' | 'audio')[] = [];
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
- if (this.inputTypes.length === 0){
91
- this.inputTypes = ['voice'];
92
- }
93
- }
94
- } catch (error) {
95
- this.hasError = true;
96
- this.statusMessage = (this.language == 'en' ? `Initialization error: ${error.message}` : `Erreur d'initialisation: ${error.message}`);
97
- this.updateDebugInfo('Initialization Error', { error: error.message });
98
- }
99
- }
100
-
101
- private updateDebugInfo(action: string, data: any) {
102
- if (this.debug) {
103
- this.debugInfo = {
104
- ...this.debugInfo,
105
- [action]: {
106
- timestamp: new Date().toISOString(),
107
- data
108
- }
109
- };
110
- }
111
- }
112
-
113
- private async handleRecordClick() {
114
- if (this.isProcessing) return;
115
-
116
- if (this.isRecording) {
117
- await this.stopRecordingAndProcess();
118
- } else {
119
- await this.startRecording();
120
- }
121
- }
122
-
123
- private async startRecording() {
124
- try {
125
- this.hasError = false;
126
- this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
127
- this.updateDebugInfo('Start Recording Attempt', {});
128
-
129
- await this.audioRecorder.startRecording();
130
-
131
- this.isRecording = true;
132
- this.statusMessage = (this.language == 'en' ? 'Recording... Click to stop' : 'Enregistrement ... Cliquer pour stopper');
133
- this.updateDebugInfo('Recording Started', {});
134
-
135
- this.recordingStateChanged.emit({
136
- isRecording: true,
137
- state: 'recording'
138
- });
139
- } catch (error) {
140
- this.hasError = true;
141
- this.statusMessage = (this.language == 'en' ? `Recording failed: ${error.message}` : `Echec de l'enregistrement : ${error.message}`);
142
- this.updateDebugInfo('Recording Error', { error: error.message });
143
-
144
- this.formFilled.emit({
145
- success: false,
146
- error: error.message
147
- });
148
- }
149
- }
150
-
151
- private async processJsonForm(jsonForm: string){
152
- console.log("processJsonForm", jsonForm);
153
- try {
154
- this.isProcessing = true;
155
- this.statusMessage = (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
156
-
157
- // Fill form using LLM
158
- this.statusMessage = (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
159
- const trimmedSchema = this.trimSchemaForAI(this.parsedSchema);
160
- const filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
161
-
162
- // Extract filled data
163
- this.filledData = this.extractFilledData(filledSchema);
164
- this.updateDebugInfo('Form Filled', {
165
- filledSchema,
166
- extractedData: this.filledData
167
- });
168
-
169
- this.parsedSchema = this.filledData;
170
-
171
- this.statusMessage = (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
172
- this.hasError = false;
173
-
174
- // Emit success event
175
- this.formFilled.emit({
176
- success: true,
177
- data: this.filledData,
178
- jsonForm: jsonForm
179
- });
180
-
181
- } catch (error) {
182
- this.hasError = true;
183
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
184
-
185
- this.formFilled.emit({
186
- success: false,
187
- error: error.message,
188
- jsonForm: jsonForm
189
- });
190
- } finally {
191
- this.isProcessing = false;
192
- }
193
- }
194
-
195
- private async processAudioContent(audioFile) {
196
- this.updateDebugInfo('Audio Captured', {
197
- size: audioFile.size,
198
- type: audioFile.type
199
- });
200
-
201
- // Transcribe audio
202
- this.statusMessage = (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
203
- const transcription = await this.speechToTextService.transcribe(audioFile, this.language);
204
- this.transcription = transcription;
205
- this.updateDebugInfo('Transcription Complete', { transcription });
206
-
207
- if (!transcription.trim()) {
208
- throw new Error('No speech detected in the recording');
209
- }
210
-
211
- // Fill form using LLM
212
- this.statusMessage = (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
213
- const trimmedSchema = this.trimSchemaForAI(this.parsedSchema);
214
- const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
215
-
216
- // Extract filled data
217
- this.filledData = this.extractFilledData(filledSchema);
218
- this.updateDebugInfo('Form Filled', {
219
- filledSchema,
220
- extractedData: this.filledData
221
- });
222
-
223
- this.parsedSchema = this.filledData;
224
-
225
- this.statusMessage = (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
226
- this.hasError = false;
227
-
228
- // Emit success event
229
- this.formFilled.emit({
230
- success: true,
231
- data: this.filledData,
232
- transcription: transcription
233
- });
234
- }
235
-
236
- private async stopRecordingAndProcess() {
237
- try {
238
- this.isRecording = false;
239
- this.isProcessing = true;
240
- this.statusMessage = (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
241
- this.updateDebugInfo('Stop Recording', {});
242
-
243
- this.recordingStateChanged.emit({
244
- isRecording: false,
245
- state: 'processing'
246
- });
247
-
248
- // Stop recording and get audio blob
249
- const audioBlob = await this.audioRecorder.stopRecording();
250
- const audioContent = new File([audioBlob], 'audio.webm', { type: 'audio/webm' });
251
- this.processAudioContent(audioContent);
252
-
253
- } catch (error) {
254
- this.hasError = true;
255
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
256
- this.updateDebugInfo('Processing Error', { error: error.message });
257
-
258
- this.formFilled.emit({
259
- success: false,
260
- error: error.message,
261
- transcription: this.transcription
262
- });
263
- } finally {
264
- this.isProcessing = false;
265
- this.recordingStateChanged.emit({
266
- isRecording: false,
267
- state: 'idle'
268
- });
269
- }
270
- }
271
-
272
- private extractFilledData(filledData: any): any {
273
- // console.log("extractFilledData", filledData);
274
- const updatedSchema = JSON.parse(JSON.stringify(this.parsedSchema));
275
- switch(this.context){
276
- case "ecoteka":
277
- // console.log("TODO extractFilledData", filledData);
278
- case "ng":
279
- if (filledData?.fields) {
280
- // Map AI response back to original schema structure
281
- filledData.fields.forEach((field: any) => {
282
- const originalField = updatedSchema.Children.find((child: any) =>
283
- child.System_Name === field.name || child.Settings?.Label === field.name
284
- );
285
-
286
- if (originalField && field.value !== undefined && field.value !== null && field.value !== '') {
287
- if (!originalField.Settings) originalField.Settings = {};
288
- originalField.Settings.Default_Value = field.value;
289
- }
290
- });
291
- }
292
-
293
- break;
294
- case "ecoll-veto":
295
- // console.log("TODO extractFilledData", filledData, updatedSchema);
296
- if (filledData?.fields) {
297
- // Map AI response back to original schema structure
298
- filledData.fields.forEach((field: any) => {
299
- let originalField = updatedSchema[0].items.find((child: any) =>
300
- child.label === field.name
301
- );
302
- if (!originalField)
303
- originalField = updatedSchema[1].items.find((child: any) =>
304
- child.label === field.name
305
- );
306
-
307
- if (originalField && field.value !== undefined && field.value !== null && field.value !== '') {
308
- updatedSchema[2][originalField.name] = field.value;
309
- }
310
- });
311
- }
312
- break;
313
- case "track":
314
- default:
315
- const data: FilledFormData = {};
316
-
317
- Object.entries(filledData.fields).forEach(([fieldID, field]: [string, any]) => {
318
- if (field.default !== undefined && field.default !== null && field.default !== '') {
319
- updatedSchema[fieldID] = field.default;
320
- }
321
- if (field.value !== undefined && field.value !== null && field.value !== '') {
322
- for (const key in updatedSchema.fields) {
323
- const schemaField = updatedSchema.fields[key];
324
- if (schemaField.title === field.name) {
325
- schemaField.value = field.value || field.default;
326
- schemaField.default = field.value || field.default;
327
- break; // stop after finding the first match
328
- }
329
- }
330
- }
331
- });
332
-
333
- break;
334
- }
335
-
336
- // console.log("extractFilledData result", updatedSchema);
337
- return updatedSchema;
338
- }
339
-
340
- private trimSchemaForAI(schema: any): any {
341
- // console.log("trimSchemaForAI", schema);
342
- switch(this.context){
343
- case "ecoteka":
344
- // console.log("TODO trimSchemaForAI", schema)
345
- case "ng":
346
- const trimmed = { fields: [] };
347
- schema.Children.forEach((child: any) => {
348
- if (!child.System_Name || !child.Type) return;
349
-
350
- const fieldData: any = {
351
- name: child.Label || child.Settings?.Label || child.System_Name,
352
- type: this.mapFieldType(child.Type)
353
- };
354
-
355
- // Add options for classification/select fields
356
- const selectTypes = ['InputClassification', 'select']
357
- if (selectTypes.includes(child.Type) && child.Children && child.Children.length > 0) {
358
- fieldData.options = child.Children.map((option: any) =>
359
- option.System_Name || option.Label || option.toString()
360
- );
361
- }
362
-
363
- console.log("fieldData", fieldData);
364
-
365
- trimmed.fields.push(fieldData);
366
- });
367
-
368
- // console.log("Schema apres transformation, contexte NG:", trimmed);
369
- return trimmed;
370
- case "ecoll-veto":
371
- // console.log("TODO trimSchemaForAI", schema)
372
- const mergedItemsSchema = (this.parsedSchema[0].items).concat(this.parsedSchema[1].items);
373
-
374
- if (mergedItemsSchema) {
375
- const trimmedSchema: FormSchema = {
376
- title: 'Form Name',
377
- description: 'Form Description',
378
- schema: {}
379
- };
380
-
381
- Object.entries(mergedItemsSchema).forEach(([key, field]: [string, any]) => {
382
- const fieldName = field.name;
383
- const fieldType = this.mapFieldType(field.type) as any;
384
- const fieldLabel = field.label || fieldName;
385
- const isRequired = field.required || false;
386
- const fieldValue = ''; //TODO
387
-
388
- trimmedSchema.schema[fieldName] = {
389
- type: fieldType,
390
- title: fieldLabel,
391
- options: field.options,
392
- readonly: field.readonly === true,
393
- default: '',
394
- };
395
- });
396
-
397
- // console.log("Schema apres transformation, contexte Track:", trimmedSchema);
398
- return trimmedSchema;
399
- }
400
- case "track":
401
- default:
402
- // Handle simple schema format (backward compatibility)
403
- if (schema?.schema ?? schema?.fields) {
404
- const trimmedSchema: FormSchema = {
405
- title: schema.title,
406
- description: schema.description,
407
- schema: {}
408
- };
409
-
410
- const finalSchema = schema?.schema ?? schema?.fields;
411
-
412
- Object.entries(finalSchema).forEach(([fieldName, field]: [string, any]) => {
413
- trimmedSchema.schema[fieldName] = {
414
- type: field.type,
415
- title: field.title,
416
- options: field.options,
417
- readonly: field.Enabled === false,
418
- default: field.DefaultValue,
419
- pattern: field.Mask,
420
- min: field.ValidationMin,
421
- max: field.ValidationMax
422
- };
423
- });
424
-
425
-
426
- // console.log("Schema apres transformation, contexte Track:", trimmedSchema);
427
- return trimmedSchema;
428
- }
429
- break;
430
- }
431
-
432
- return schema;
433
- }
434
-
435
- private mapFieldType(type: string): string {
436
- const typeMapping = {
437
- 'InputAutocomplete': 'string',
438
- 'InputInteger': 'number',
439
- 'InputTextArea': 'string',
440
- 'InputDateTimePicker': 'datetime',
441
- 'InputDecimal': 'number',
442
- 'InputClassification': 'select',
443
- 'InputCheckbox': 'boolean',
444
- 'InputTextTranslation': 'string',
445
- 'thesaurus': 'string',
446
- 'position': 'string',
447
- 'text': 'string',
448
- 'textarea': 'string',
449
- 'number': 'number',
450
- 'date': 'date',
451
- 'datetime': 'datetime',
452
- 'select': 'select',
453
- 'checkbox': 'checkbox'
454
- };
455
-
456
- return typeMapping[type] || 'string';
457
- }
458
-
459
- // Utility methods exposed as public API
460
- @Method() public async convertXmlToJson(xmlForm: string): Promise<FormSchemaFieldsOnly> {
461
- return SchemaConverter.convertXmlToJson(xmlForm, this.classificationRootUrl, this.language);
462
- }
463
-
464
- @Method() public async convertJsonToXml(jsonForm: FormSchemaFieldsOnlyExtended): Promise<string> {
465
- return SchemaConverter.convertJsonToXml(jsonForm);
466
- }
467
-
468
- // Utility methods exposed as public API
469
- @Method() public async convertXmlToJsonLegacy(xmlForm: string): Promise<FormSchema> {
470
- return SchemaConverter.convertXmlToJsonLegacy(xmlForm);
471
- }
472
-
473
- @Method() public async convertJsonToXmlLegacy(jsonForm: FormSchema): Promise<string> {
474
- return SchemaConverter.convertJsonToXmlLegacy(jsonForm);
475
- }
476
-
477
- private renderUploadButton() {
478
- if (!this.inputTypes.includes('ocr'))
479
- return;
480
- return(
481
- <ocr-file-uploader
482
- batch={false}
483
- callback={(data) => {this.processJsonForm(data);}}
484
- >
485
- </ocr-file-uploader>
486
- );
487
- }
488
-
489
- private fileInputAudioRecord!: HTMLInputElement;
490
-
491
- private triggerAudioRecordUpload = () => {
492
- this.fileInputAudioRecord.click();
493
- };
494
-
495
- private handleAudioRecordChange = async (event: Event) => {
496
- const input = event.target as HTMLInputElement;
497
- if (!input.files || input.files.length === 0) return;
498
-
499
- const file = input.files[0];
500
-
501
- // Here you can handle the file upload to your API
502
- console.log('Selected file:', file);
503
-
504
- this.processAudioContent(file);
505
- };
506
-
507
- private renderUploadRecordButton() {
508
- return (
509
- <div class="upload-record-container" onClick={this.triggerAudioRecordUpload}>
510
- <input
511
- type="file"
512
- ref={el => (this.fileInputAudioRecord = el!)}
513
- onChange={this.handleAudioRecordChange}
514
- style={{ display: 'none' }}
515
- />
516
- <div class='upload-record-button'>
517
- <svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
518
- <path fill-rule="evenodd" clip-rule="evenodd" d="M12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C12.6345 21.25 13.2534 21.1862 13.8508 21.065C14.2567 20.9826 14.6526 21.2448 14.735 21.6508C14.8174 22.0567 14.5551 22.4526 14.1492 22.535C13.4541 22.6761 12.7353 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 12.7353 22.6761 13.4541 22.535 14.1492C22.4526 14.5551 22.0567 14.8174 21.6508 14.735C21.2448 14.6526 20.9826 14.2567 21.065 13.8508C21.1862 13.2534 21.25 12.6345 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75ZM12.7676 8.52584C12.7661 8.53409 12.7604 8.56729 12.7564 8.64336C12.7502 8.76101 12.75 8.91982 12.75 9.17082C12.75 9.27795 12.7501 9.33904 12.7518 9.38529C12.7528 9.41425 12.7542 9.42649 12.7546 9.42955C12.7662 9.48945 12.7993 9.54303 12.8477 9.58021C12.8502 9.58194 12.8606 9.58864 12.886 9.60252C12.9266 9.62468 12.9812 9.65209 13.0771 9.7L14.3938 10.3584C14.6183 10.4706 14.7604 10.5414 14.8684 10.5885C14.9382 10.6189 14.9705 10.6287 14.9785 10.631C15.0885 10.6405 15.1917 10.5767 15.2324 10.4742C15.2339 10.4659 15.2396 10.4327 15.2436 10.3566C15.2498 10.239 15.25 10.0802 15.25 9.82918C15.25 9.72205 15.2499 9.66096 15.2482 9.61471C15.2472 9.58575 15.2458 9.57351 15.2454 9.57045C15.2338 9.51055 15.2007 9.45697 15.1523 9.41979C15.1498 9.41805 15.1394 9.41136 15.114 9.39748C15.0734 9.37533 15.0188 9.34791 14.9229 9.3L13.6062 8.64164C13.3817 8.52939 13.2396 8.45859 13.1316 8.41151C13.0617 8.38107 13.0295 8.37131 13.0215 8.36896C12.9115 8.35945 12.8083 8.42327 12.7676 8.52584ZM12.75 11.2135L13.7396 11.7083C13.9425 11.8098 14.1204 11.8987 14.269 11.9635C14.4199 12.0293 14.5988 12.097 14.7972 12.1202C15.6037 12.2142 16.3689 11.7413 16.6454 10.978C16.7134 10.7901 16.7328 10.5998 16.7415 10.4355C16.75 10.2735 16.75 10.0747 16.75 9.8479V9.82918C16.75 9.81565 16.75 9.80205 16.75 9.78837C16.7503 9.62647 16.7505 9.45474 16.7188 9.28904C16.638 8.86674 16.4045 8.48898 16.0629 8.22783C15.9289 8.12535 15.7752 8.04877 15.6303 7.97658C15.618 7.97048 15.6059 7.96441 15.5938 7.95836L14.2603 7.29164C14.0575 7.19022 13.8796 7.10128 13.731 7.03647C13.5801 6.97071 13.4012 6.90297 13.2028 6.87982C12.3963 6.78575 11.6311 7.25868 11.3546 8.02203C11.2866 8.20986 11.2672 8.40019 11.2585 8.56454C11.2519 8.68919 11.2504 8.83571 11.2501 9L11.25 9.11944C11.25 9.13026 11.25 9.14115 11.25 9.1521V9.17082C11.25 9.18435 11.25 9.19795 11.25 9.21163C11.2499 9.23918 11.2499 9.26701 11.25 9.29505V12.5499C10.875 12.3581 10.4501 12.25 10 12.25C8.48122 12.25 7.25 13.4812 7.25 15C7.25 16.5188 8.48122 17.75 10 17.75C11.5188 17.75 12.75 16.5188 12.75 15V11.2135ZM11.25 15C11.25 14.3096 10.6904 13.75 10 13.75C9.30964 13.75 8.75 14.3096 8.75 15C8.75 15.6904 9.30964 16.25 10 16.25C10.6904 16.25 11.25 15.6904 11.25 15ZM17.4697 14.4697C17.7626 14.1768 18.2374 14.1768 18.5303 14.4697L21.0303 16.9697C21.3232 17.2626 21.3232 17.7374 21.0303 18.0303C20.7374 18.3232 20.2626 18.3232 19.9697 18.0303L18.75 16.8107V22C18.75 22.4142 18.4142 22.75 18 22.75C17.5858 22.75 17.25 22.4142 17.25 22V16.8107L16.0303 18.0303C15.7374 18.3232 15.2626 18.3232 14.9697 18.0303C14.6768 17.7374 14.6768 17.2626 14.9697 16.9697L17.4697 14.4697Z" />
519
- </svg>
520
- </div>
521
- </div>
522
- )
523
- }
524
-
525
- private renderRecordButton() {
526
- if (!this.inputTypes.includes('voice'))
527
- return;
528
-
529
- const buttonClass = [
530
- 'record-button',
531
- this.isRecording && 'recording',
532
- this.isProcessing && 'processing'
533
- ].filter(Boolean).join(' ');
534
-
535
- const isDisabled = this.isProcessing || this.hasError;
536
-
537
- return (
538
- <button
539
- class={buttonClass}
540
- onClick={() => this.handleRecordClick()}
541
- disabled={isDisabled}
542
- aria-label={this.isRecording ? 'Stop recording' : 'Start recording'}
543
- >
544
- {this.isProcessing ? (
545
- <svg class="record-icon" viewBox="0 0 24 24">
546
- <circle cx="12" cy="12" r="3">
547
- <animate attributeName="r" values="3;6;3" dur="1s" repeatCount="indefinite" />
548
- <animate attributeName="opacity" values="1;0.3;1" dur="1s" repeatCount="indefinite" />
549
- </circle>
550
- </svg>
551
- ) : this.isRecording ? (
552
- <svg class="record-icon" viewBox="0 0 24 24">
553
- <rect x="6" y="6" width="12" height="12" rx="2" />
554
- </svg>
555
- ) : (
556
- <svg class="record-icon" viewBox="0 0 24 24">
557
- <circle cx="12" cy="12" r="8" />
558
- </svg>
559
- )}
560
- </button>
561
- );
562
- }
563
-
564
- private renderStatusMessage() {
565
- const statusClass = [
566
- 'status-text',
567
- this.hasError && 'error',
568
- this.filledData && !this.hasError && 'success'
569
- ].filter(Boolean).join(' ');
570
-
571
- return <div class={statusClass}>{this.statusMessage}</div>;
572
- }
573
-
574
- private renderFormPreview() {
575
- if (!this.parsedSchema) return null;
576
-
577
- const isPreview = this.isReadonlyMode && !this.filledData;
578
- const title = isPreview ? 'Form Preview (Voice input to fill)' : 'Voice-Filled Form:';
579
-
580
- return (
581
- <div class="form-preview">
582
- <div class="form-preview-title">{title}</div>
583
- <form class="voice-filled-form">
584
- {this.renderFormFields()}
585
- </form>
586
- </div>
587
- );
588
- }
589
-
590
- private renderFormFields() {
591
- // console.log("renderFormFields", this.parsedSchema);
592
- if (!this.parsedSchema) return null;
593
-
594
- switch(this.context){
595
- case "ecoteka":
596
- // console.log("TODO renderFormFields", this.parsedSchema);
597
- case "ng":
598
- return this.parsedSchema.Children.map((child: any) => {
599
- if (!child.System_Name || !child.Type) return null;
600
-
601
- const fieldName = child.System_Name;
602
- const fieldType = this.mapFieldType(child.Type);
603
- const fieldLabel = child.Settings?.Label || child.System_Name;
604
- const isRequired = child.Required || false;
605
- const fieldValue = child.Settings?.Default_Value;
606
-
607
- return (
608
- <div class="form-group" key={fieldName}>
609
- <label htmlFor={fieldName} class="form-label">
610
- {fieldLabel}
611
- {isRequired && <span class="required">*</span>}
612
- </label>
613
- {this.renderFormField(fieldName, {
614
- type: fieldType,
615
- title: fieldLabel,
616
- required: isRequired,
617
- options: child.Children?.map((option: any) =>
618
- option.System_Name || option.Label || option.toString()
619
- )
620
- }, fieldValue)}
621
- </div>
622
- );
623
- }).filter(Boolean);
624
- case "ecoll-veto":
625
- // NOTE STEP 2
626
- // console.log("TODO renderFormFields", this.parsedSchema);
627
- const mergedItemsSchema = (this.parsedSchema[0].items).concat(this.parsedSchema[1].items);
628
- return Object.entries(mergedItemsSchema).map(([key, field]: [string, any]) => {
629
-
630
- const fieldName = field.name;
631
- field.type = this.mapFieldType(field.type);
632
- const fieldLabel = field.label || fieldName;
633
- const isRequired = field.required || false;
634
- const fieldValue = this.parsedSchema[2][fieldName];
635
-
636
- return (
637
- <div class="form-group" key={fieldName}>
638
- <label htmlFor={fieldName} class="form-label">
639
- { fieldLabel }
640
- {isRequired && <span class="required">*</span>}
641
- </label>
642
- {this.renderFormField(fieldName, field, (this.filledData?.[fieldName] ?? fieldValue))}
643
- </div>
644
- );
645
- });
646
- case "track":
647
- default:
648
- return Object.entries(this.parsedSchema.fields).map(([fieldName, field]: [string, any]) => (
649
- <div class="form-group" key={fieldName}>
650
- <label htmlFor={fieldName} class="form-label">
651
- {field.title || fieldName}
652
- {field.required && <span class="required">*</span>}
653
- </label>
654
- {this.renderFormField(fieldName, field, (this.filledData?.[fieldName] ?? field.value))}
655
- </div>
656
- ));
657
- }
658
- }
659
-
660
- private renderFormField(fieldName: string, field: any, value: any) {
661
- const isReadonly = this.isReadonlyMode && !this.filledData;
662
- const commonProps = {
663
- id: fieldName,
664
- name: fieldName,
665
- class: 'form-input',
666
- required: field.required,
667
- disabled: isReadonly
668
- };
669
-
670
- switch (field.type) {
671
- case 'select':
672
- if (isReadonly) {
673
- // In readonly mode, show all options as a list instead of dropdown
674
- return (
675
- <div class="readonly-select">
676
- <div class="select-placeholder">Available options:</div>
677
- <ul class="select-options-list">
678
- {field.options?.map(option => (
679
- <li class="select-option">{option}</li>
680
- ))}
681
- </ul>
682
- </div>
683
- );
684
- }
685
- return (
686
- <select id={fieldName} name={fieldName} class="form-input" required={field.required}>
687
- <option value="">-- Select --</option>
688
- {field.options?.map(option => (
689
- <option value={option} selected={value === option}>
690
- {option}
691
- </option>
692
- ))}
693
- </select>
694
- );
695
-
696
- case 'boolean':
697
- return (
698
- <input
699
- {...commonProps}
700
- type="checkbox"
701
- class="form-checkbox"
702
- checked={value === true || value === 'true'}
703
- />
704
- );
705
-
706
- case 'number':
707
- return (
708
- <input
709
- {...commonProps}
710
- type="number"
711
- min={field.min}
712
- max={field.max}
713
- step="any"
714
- value={value || ''}
715
- />
716
- );
717
-
718
- case 'date':
719
- return (
720
- <input
721
- {...commonProps}
722
- type='date'
723
- value={value ? (([d, m, y]) => `${y}-${m}-${d}`)(value.split("/")) : ''}
724
- />
725
- );
726
-
727
- case 'datetime':
728
- return (
729
- <input
730
- {...commonProps}
731
- type='datetime-local'
732
- value={value || ''}
733
- />
734
- );
735
-
736
- default: // string
737
- return (
738
- <input
739
- {...commonProps}
740
- type="text"
741
- pattern={field.pattern}
742
- placeholder={field.description}
743
- value={value || ''}
744
- />
745
- );
746
- }
747
- }
748
-
749
- private renderDebugPanel() {
750
- if (!this.debug) return null;
751
-
752
- return (
753
- <div class="debug-panel">
754
- <div class="debug-title">Debug Information:</div>
755
- <div class="debug-content">
756
- {JSON.stringify(this.debugInfo, null, 2)}
757
- </div>
758
- </div>
759
- );
760
- }
761
-
762
- render() {
763
- return (
764
- <div>
765
- <div class={"voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : "")}>
766
- <div class="row-audio-area">
767
- {this.renderRecordButton()}
768
- {this.renderUploadRecordButton()}
769
- </div>
770
- {this.displayStatus ? this.renderStatusMessage() : ""}
771
- {this.renderUploadButton()}
772
- {this.renderForm ? this.renderFormPreview() : ""}
773
- {this.debug ? this.renderDebugPanel() : ""}
774
- </div>
775
- </div>
776
- );
777
- }
778
- }