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.
- package/.editorconfig +15 -0
- package/.prettierrc.json +13 -0
- package/LICENSE +21 -0
- package/api-key-inject.js +46 -0
- package/dist/cjs/app-globals-V2Kpy_OQ.js +8 -0
- package/dist/cjs/app-globals-V2Kpy_OQ.js.map +1 -0
- package/dist/cjs/file-uploader.voice-input-module.entry.cjs.js.map +1 -0
- package/dist/cjs/file-uploader_2.cjs.entry.js +1319 -0
- package/dist/cjs/file-uploader_2.cjs.entry.js.map +1 -0
- package/dist/cjs/index-BTSzTkSZ.js +1494 -0
- package/dist/cjs/index-BTSzTkSZ.js.map +1 -0
- package/dist/cjs/index.cjs.js +5 -0
- package/dist/cjs/index.cjs.js.map +1 -0
- package/dist/cjs/loader.cjs.js +16 -0
- package/dist/cjs/loader.cjs.js.map +1 -0
- package/dist/cjs/voice-input-module.cjs.js +28 -0
- package/dist/cjs/voice-input-module.cjs.js.map +1 -0
- package/dist/collection/collection-manifest.json +13 -0
- package/dist/collection/components/file-uploader/file-uploader.css +26 -0
- package/dist/collection/components/file-uploader/file-uploader.js +130 -0
- package/dist/collection/components/file-uploader/file-uploader.js.map +1 -0
- package/dist/collection/components/voice-input-module/voice-input-module.css +251 -0
- package/dist/collection/components/voice-input-module/voice-input-module.js +875 -0
- package/dist/collection/components/voice-input-module/voice-input-module.js.map +1 -0
- package/dist/collection/index.js +12 -0
- package/dist/collection/index.js.map +1 -0
- package/dist/collection/services/audio-recorder.service.js +66 -0
- package/dist/collection/services/audio-recorder.service.js.map +1 -0
- package/dist/collection/services/llm.service.js +193 -0
- package/dist/collection/services/llm.service.js.map +1 -0
- package/dist/collection/services/speech-to-text.service.js +62 -0
- package/dist/collection/services/speech-to-text.service.js.map +1 -0
- package/dist/collection/types/form-schema.types.js +2 -0
- package/dist/collection/types/form-schema.types.js.map +1 -0
- package/dist/collection/types/service-providers.types.js +2 -0
- package/dist/collection/types/service-providers.types.js.map +1 -0
- package/dist/collection/utils/schema-converter.js +422 -0
- package/dist/collection/utils/schema-converter.js.map +1 -0
- package/dist/components/file-uploader.d.ts +11 -0
- package/dist/components/file-uploader.js +9 -0
- package/dist/components/file-uploader.js.map +1 -0
- package/dist/components/file-uploader2.js +98 -0
- package/dist/components/file-uploader2.js.map +1 -0
- package/dist/components/index.d.ts +33 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/voice-input-module.d.ts +11 -0
- package/dist/components/voice-input-module.js +1292 -0
- package/dist/components/voice-input-module.js.map +1 -0
- package/dist/esm/app-globals-DQuL1Twl.js +6 -0
- package/dist/esm/app-globals-DQuL1Twl.js.map +1 -0
- package/dist/esm/file-uploader.voice-input-module.entry.js.map +1 -0
- package/dist/esm/file-uploader_2.entry.js +1316 -0
- package/dist/esm/file-uploader_2.entry.js.map +1 -0
- package/dist/esm/index-jmc2yzBp.js +1487 -0
- package/dist/esm/index-jmc2yzBp.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/loader.js +14 -0
- package/dist/esm/loader.js.map +1 -0
- package/dist/esm/voice-input-module.js +24 -0
- package/dist/esm/voice-input-module.js.map +1 -0
- package/dist/index.cjs.js +1 -0
- package/dist/index.js +1 -0
- package/dist/types/components/file-uploader/file-uploader.d.ts +8 -0
- package/dist/types/components/voice-input-module/voice-input-module.d.ts +55 -0
- package/dist/types/components.d.ts +158 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/services/audio-recorder.service.d.ts +9 -0
- package/dist/types/services/llm.service.d.ts +15 -0
- package/dist/types/services/speech-to-text.service.d.ts +11 -0
- package/dist/types/stencil-public-runtime.d.ts +1709 -0
- package/dist/types/types/form-schema.types.d.ts +70 -0
- package/dist/types/types/service-providers.types.d.ts +20 -0
- package/dist/types/utils/schema-converter.d.ts +22 -0
- package/dist/voice-input-module/file-uploader.voice-input-module.entry.esm.js.map +1 -0
- package/dist/voice-input-module/index.esm.js +2 -0
- package/dist/voice-input-module/index.esm.js.map +1 -0
- package/dist/voice-input-module/loader.esm.js.map +1 -0
- package/dist/voice-input-module/p-7b4f33ba.entry.js +2 -0
- package/dist/voice-input-module/p-7b4f33ba.entry.js.map +1 -0
- package/dist/voice-input-module/p-DQuL1Twl.js +2 -0
- package/dist/voice-input-module/p-DQuL1Twl.js.map +1 -0
- package/dist/voice-input-module/p-jmc2yzBp.js +3 -0
- package/dist/voice-input-module/p-jmc2yzBp.js.map +1 -0
- package/dist/voice-input-module/voice-input-module.esm.js +2 -0
- package/dist/voice-input-module/voice-input-module.esm.js.map +1 -0
- package/env-config.js +4 -0
- package/inject-env.js +20 -0
- package/package.json +37 -0
- package/readme.md +111 -0
- package/src/components/file-uploader/file-uploader.css +26 -0
- package/src/components/file-uploader/file-uploader.tsx +100 -0
- package/src/components/file-uploader/readme.md +31 -0
- package/src/components/voice-input-module/readme.md +114 -0
- package/src/components/voice-input-module/voice-input-module.css +251 -0
- package/src/components/voice-input-module/voice-input-module.tsx +731 -0
- package/src/components.d.ts +158 -0
- package/src/index.html +663 -0
- package/src/index.ts +12 -0
- package/src/services/audio-recorder.service.ts +74 -0
- package/src/services/llm.service.ts +221 -0
- package/src/services/speech-to-text.service.ts +72 -0
- package/src/types/form-schema.types.ts +78 -0
- package/src/types/service-providers.types.ts +22 -0
- package/src/utils/schema-converter.ts +494 -0
- package/stencil.config.ts +24 -0
- 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
|
+
}
|