reneco-advanced-input-module 0.0.21 → 0.0.23

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 (44) hide show
  1. package/dist/cjs/loader.cjs.js +1 -1
  2. package/dist/cjs/voice-input-module.cjs.entry.js +481 -154
  3. package/dist/cjs/voice-input-module.cjs.entry.js.map +1 -1
  4. package/dist/cjs/voice-input-module.cjs.js +1 -1
  5. package/dist/cjs/voice-input-module.entry.cjs.js.map +1 -1
  6. package/dist/collection/components/voice-input-module/voice-input-module.js +295 -96
  7. package/dist/collection/components/voice-input-module/voice-input-module.js.map +1 -1
  8. package/dist/collection/services/audio-recorder.service.js +61 -44
  9. package/dist/collection/services/audio-recorder.service.js.map +1 -1
  10. package/dist/collection/services/llm.service.js +137 -8
  11. package/dist/collection/services/llm.service.js.map +1 -1
  12. package/dist/collection/services/speech-to-text.service.js +39 -5
  13. package/dist/collection/services/speech-to-text.service.js.map +1 -1
  14. package/dist/collection/types/service-providers.types.js +9 -1
  15. package/dist/collection/types/service-providers.types.js.map +1 -1
  16. package/dist/components/voice-input-module.js +489 -155
  17. package/dist/components/voice-input-module.js.map +1 -1
  18. package/dist/esm/loader.js +1 -1
  19. package/dist/esm/voice-input-module.entry.js +481 -154
  20. package/dist/esm/voice-input-module.entry.js.map +1 -1
  21. package/dist/esm/voice-input-module.js +1 -1
  22. package/dist/types/components/voice-input-module/voice-input-module.d.ts +6 -0
  23. package/dist/types/components.d.ts +18 -0
  24. package/dist/types/services/audio-recorder.service.d.ts +5 -0
  25. package/dist/types/services/llm.service.d.ts +22 -0
  26. package/dist/types/services/speech-to-text.service.d.ts +8 -0
  27. package/dist/types/types/service-providers.types.d.ts +6 -2
  28. package/dist/voice-input-module/p-920964b2.entry.js +3 -0
  29. package/dist/voice-input-module/p-920964b2.entry.js.map +1 -0
  30. package/dist/voice-input-module/voice-input-module.entry.esm.js.map +1 -1
  31. package/dist/voice-input-module/voice-input-module.esm.js +1 -1
  32. package/package.json +1 -1
  33. package/readme.md +81 -0
  34. package/www/build/p-920964b2.entry.js +3 -0
  35. package/www/build/p-920964b2.entry.js.map +1 -0
  36. package/www/build/p-cbf8974a.js +2 -0
  37. package/www/build/voice-input-module.entry.esm.js.map +1 -1
  38. package/www/build/voice-input-module.esm.js +1 -1
  39. package/www/index.html +12 -1
  40. package/dist/voice-input-module/p-4e449895.entry.js +0 -3
  41. package/dist/voice-input-module/p-4e449895.entry.js.map +0 -1
  42. package/www/build/p-3a11e8d2.js +0 -2
  43. package/www/build/p-4e449895.entry.js +0 -3
  44. package/www/build/p-4e449895.entry.js.map +0 -1
@@ -1,68 +1,95 @@
1
1
  import { r as registerInstance, c as createEvent, h as h$1 } from './index-jmc2yzBp.js';
2
2
 
3
+ const TRANSCRIPTION_MODELS = {
4
+ openai: ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe', 'whisper-1'],
5
+ mistral: ['voxtral-mini-latest', 'voxtral-mini-transcribe-realtime-latest'],
6
+ };
7
+ const COMPLETION_MODELS = {
8
+ openai: ['gpt-5', 'gpt-5-mini', 'gpt-5.5', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o', 'gpt-4o-mini', 'o4-mini', 'gpt-5.2', 'gpt-5.3', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-pro', 'gpt-5.4-nano'],
9
+ anthropic: ['claude-opus-4', 'claude-sonnet-4', 'claude-haiku-4', 'claude-3.7-sonnet', 'claude-3.5-sonnet', 'claude-opus-4.7', 'claude-opus-4.6', 'claude-opus-4.5', 'claude-sonnet-4.6', 'claude-sonnet-4.5'],
10
+ mistral: ['mistral-large-latest', 'mistral-medium-latest', 'mistral-small-latest', 'ministral', 'mistral-nemo'],
11
+ };
12
+
3
13
  class AudioRecorderService {
4
14
  constructor() {
5
15
  this.mediaRecorder = null;
6
16
  this.audioChunks = [];
7
17
  this.stream = null;
18
+ this.audioContext = null;
19
+ this.scriptProcessor = null;
20
+ this.pcmChunks = [];
21
+ this.sampleRate = 16000;
8
22
  }
9
23
  async startRecording() {
10
- try {
11
- // Check if the API exists before calling
12
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
13
- console.error('Failed to start recording:', 'Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
14
- return; // Exit gracefully instead of throwing
15
- }
16
- this.stream = await navigator.mediaDevices.getUserMedia({
17
- audio: {
18
- echoCancellation: true,
19
- noiseSuppression: true,
20
- autoGainControl: true
21
- }
22
- });
23
- this.audioChunks = [];
24
- this.mediaRecorder = new MediaRecorder(this.stream, {
25
- mimeType: 'audio/webm;codecs=opus'
26
- });
27
- this.mediaRecorder.ondataavailable = (event) => {
28
- if (event.data && event.data.size > 0) {
29
- this.audioChunks.push(event.data);
30
- }
31
- };
32
- this.mediaRecorder.start(100); // Collect data every 100ms
33
- }
34
- catch (error) {
35
- console.error('Failed to start recording:', error);
24
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
25
+ throw new Error('Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
36
26
  }
27
+ this.stream = await navigator.mediaDevices.getUserMedia({
28
+ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
29
+ });
30
+ this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
31
+ const source = this.audioContext.createMediaStreamSource(this.stream);
32
+ this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
33
+ this.pcmChunks = [];
34
+ this.scriptProcessor.onaudioprocess = (e) => {
35
+ const input = e.inputBuffer.getChannelData(0);
36
+ this.pcmChunks.push(new Float32Array(input));
37
+ };
38
+ source.connect(this.scriptProcessor);
39
+ this.scriptProcessor.connect(this.audioContext.destination);
37
40
  }
38
41
  async stopRecording() {
39
- return new Promise((resolve, reject) => {
40
- if (!this.mediaRecorder) {
41
- reject(new Error('No active recording found'));
42
- return;
42
+ if (!this.audioContext || !this.scriptProcessor) {
43
+ throw new Error('No active recording found');
44
+ }
45
+ this.scriptProcessor.disconnect();
46
+ await this.audioContext.close();
47
+ const wavBlob = this.encodeWav(this.pcmChunks, this.sampleRate);
48
+ this.cleanup();
49
+ return wavBlob;
50
+ }
51
+ encodeWav(chunks, sampleRate) {
52
+ const totalSamples = chunks.reduce((acc, c) => acc + c.length, 0);
53
+ const buffer = new ArrayBuffer(44 + totalSamples * 2);
54
+ const view = new DataView(buffer);
55
+ const writeStr = (offset, str) => {
56
+ for (let i = 0; i < str.length; i++)
57
+ view.setUint8(offset + i, str.charCodeAt(i));
58
+ };
59
+ writeStr(0, 'RIFF');
60
+ view.setUint32(4, 36 + totalSamples * 2, true);
61
+ writeStr(8, 'WAVE');
62
+ writeStr(12, 'fmt ');
63
+ view.setUint32(16, 16, true); // PCM chunk size
64
+ view.setUint16(20, 1, true); // PCM format
65
+ view.setUint16(22, 1, true); // mono
66
+ view.setUint32(24, sampleRate, true);
67
+ view.setUint32(28, sampleRate * 2, true); // byte rate
68
+ view.setUint16(32, 2, true); // block align
69
+ view.setUint16(34, 16, true); // bits per sample
70
+ writeStr(36, 'data');
71
+ view.setUint32(40, totalSamples * 2, true);
72
+ let offset = 44;
73
+ for (const chunk of chunks) {
74
+ for (let i = 0; i < chunk.length; i++) {
75
+ const s = Math.max(-1, Math.min(1, chunk[i]));
76
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
77
+ offset += 2;
43
78
  }
44
- this.mediaRecorder.onstop = () => {
45
- const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
46
- this.cleanup();
47
- resolve(audioBlob);
48
- };
49
- this.mediaRecorder.onerror = (event) => {
50
- reject(new Error(`Recording error: ${event}`));
51
- };
52
- this.mediaRecorder.stop();
53
- });
79
+ }
80
+ return new Blob([buffer], { type: 'audio/wav' });
54
81
  }
55
82
  isRecording() {
56
- var _a;
57
- return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'recording';
83
+ return this.scriptProcessor !== null;
58
84
  }
59
85
  cleanup() {
60
86
  if (this.stream) {
61
87
  this.stream.getTracks().forEach(track => track.stop());
62
88
  this.stream = null;
63
89
  }
64
- this.mediaRecorder = null;
65
- this.audioChunks = [];
90
+ this.audioContext = null;
91
+ this.scriptProcessor = null;
92
+ this.pcmChunks = [];
66
93
  }
67
94
  }
68
95
 
@@ -124,15 +151,49 @@ class WhisperSpeechToTextService {
124
151
  }
125
152
  }
126
153
  }
154
+ class MistralSpeechToTextService {
155
+ constructor(config) {
156
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
157
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
158
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'voxtral-mini-latest';
159
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
160
+ if (!this.useProxy && !this.apiKey) {
161
+ throw new Error('Mistral API key is required for speech-to-text service');
162
+ }
163
+ }
164
+ async transcribe(audioContent, lang = 'en') {
165
+ var _a;
166
+ try {
167
+ const formData = new FormData();
168
+ formData.append('file', audioContent);
169
+ formData.append('model', this.model);
170
+ formData.append('language', lang);
171
+ const endpoint = this.useProxy ? `${this.proxyUrl}/transcribe-mistral` : 'https://api.mistral.ai/v1/audio/transcriptions';
172
+ const headers = {};
173
+ if (!this.useProxy) {
174
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
175
+ }
176
+ const response = await fetch(endpoint, { method: 'POST', headers, body: formData });
177
+ if (!response.ok) {
178
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
179
+ throw new Error(`Transcription failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
180
+ }
181
+ const result = await response.json();
182
+ return result.text || '';
183
+ }
184
+ catch (error) {
185
+ throw new Error(`Mistral speech-to-text failed: ${error.message}`);
186
+ }
187
+ }
188
+ }
127
189
  class SpeechToTextServiceFactory {
128
190
  static create(config) {
129
191
  var _a;
130
- const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || 'whisper';
192
+ const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
131
193
  switch (provider) {
132
- case 'whisper':
133
- return new WhisperSpeechToTextService(config.speechToText);
134
- default:
135
- throw new Error(`Unsupported speech-to-text provider: ${provider}`);
194
+ case 'openai': return new WhisperSpeechToTextService(config.speechToText);
195
+ case 'mistral': return new MistralSpeechToTextService(config.speechToText);
196
+ default: throw new Error(`Unsupported speech-to-text provider: ${provider}`);
136
197
  }
137
198
  }
138
199
  }
@@ -324,7 +385,8 @@ class OpenAILLMService {
324
385
  `${field.readonly ? ', readonly' : ''}` +
325
386
  `${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
326
387
  `${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
327
- `${field.max && field.max !== "" ? ', max=' + field.max : ''}`;
388
+ `${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
389
+ `${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
328
390
  if (field.options && field.options.length > 0) {
329
391
  if (field.options.length < 50) {
330
392
  description += `) - options: ${field.options.join(', ')}`;
@@ -346,7 +408,8 @@ class OpenAILLMService {
346
408
  `${field.readonly ? ', readonly' : ''}` +
347
409
  `${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
348
410
  `${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
349
- `${field.max && field.max !== "" ? ', max=' + field.max : ''}`;
411
+ `${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
412
+ `${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
350
413
  if (field.options && field.options.length > 0) {
351
414
  if (field.options.length < 50) {
352
415
  description += `) - options: ${field.options.join(', ')}`;
@@ -490,12 +553,14 @@ class OpenAILLMService {
490
553
  7. Only include fields where relevant information is found
491
554
  8. The current GMT datetime is ${new Date().toGMTString()}
492
555
  9. Respect the constraints written between parenthesis for readonly status (readonly fields MUST NOT be filled with values), min and max values, whatever the transcription says
556
+ 9b. CRITICAL - DATE/DATETIME OUT OF RANGE: For date and datetime fields with min and/or max constraints, if the value from the transcription falls outside the allowed range, leave the field EMPTY. NEVER clamp, adjust, or substitute a boundary date - either the date is valid and within range, or the field is left empty
493
557
  10. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
494
558
  11. CRITICAL - DUPLICATE FIELDS: When you see multiple fields with the same name but different IDs (e.g., "ID:field1 | Event date" and "ID:field2 | Event date"), these are DIFFERENT fields in DIFFERENT sections. The user may explicitly say which section they are filling (e.g., "for the first/second sub-form"). Listen VERY CAREFULLY to these contextual clues. Use the readonly headers to understand which section each field belongs to. If the user says "for the clinical sign, the event date is X", you must fill the Event date field that comes AFTER the "MC Clinical sign" header, NOT the first Event date you see
495
559
  12. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
496
- 13. CRITICAL: For select fields, options like "No", "Non", "Non applicable", "Inconnu", "Unknown", "N/A" are VALID VALUES that can be selected. When the user says these words, treat them as legitimate option choices, NOT always as negations or refusals to answer. HOWEVER, for non-select fields (string, number, date, etc.), if the user says "I don't know", "unknown", "not known", "inconnu", "je ne sais pas", this usually means they have NO VALUE to provide, unless explicitely precised - leave the field empty, do NOT fill it with the literal text "unknown" or "I don't know"
560
+ 13. CRITICAL - UNKNOWN/NO/N-A VALUES: The interpretation of words like "Unknown", "Inconnu", "No", "Non", "N/A", "Non applicable" depends ENTIRELY on whether the field has those words as explicit options. Rule: if the user says "field X is Unknown" (or any similar phrasing) AND "Unknown" (or a close variant) exists in the options list for that field, then SELECT that option - it is a valid value. If the user says such words for a field that does NOT have them as options (string, number, date, etc.), then it means the user has no value to provide - leave the field empty, do NOT fill it with the literal text
497
561
  14. CRITICAL: Each field has a unique ID shown as "ID:xxx" at the start of its description. You MUST include this exact ID in your response for each field you fill. The ID is the ONLY way to distinguish between fields with the same name
498
562
  15. CRITICAL - DEFAULT VALUES: Some fields have default values shown as "default=xxx". If the user does NOT mention a specific value for that field in the transcription, you MUST return the default value. Default values should be preserved unless explicitly overridden by the user
563
+ 16. CRITICAL - PATTERN VALIDATION: Some fields have a regex pattern shown as "pattern=xxx". The value you extract MUST match this pattern exactly. If the transcription is too ambiguous to produce a value that matches the pattern, leave the field EMPTY
499
564
 
500
565
  Respond with JSON in this exact format: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
501
566
  let userPrompt = `
@@ -564,10 +629,12 @@ class OpenAILLMService {
564
629
  8. Return the same schema structure with 'default' values filled
565
630
  9. The current GMT datetime is ${new Date().toGMTString()}
566
631
  10. Respect the constraints written between parenthesis for readonly status (readonly fields MUST NOT be filled with values), min and max values, whatever the transcription says
632
+ 10b. CRITICAL - DATE/DATETIME OUT OF RANGE: For date and datetime fields with min and/or max constraints, if the value from the transcription falls outside the allowed range, leave the field EMPTY. NEVER clamp, adjust, or substitute a boundary date - either the date is valid and within range, or the field is left empty
567
633
  11. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
568
634
  12. CRITICAL - DUPLICATE FIELDS: When you see multiple fields with the same name but different IDs (e.g., "ID:field1 | Event date" and "ID:field2 | Event date"), these are DIFFERENT fields in DIFFERENT sections. The user will explicitly say which section they are filling (e.g., "for the first/second sub-form", "in the anamnesis section", "for the clinical sign"). Listen VERY CAREFULLY to these contextual clues. Use the readonly headers to understand which section each field belongs to. If the user says "for the clinical sign, the event date is X", you must fill the Event date field that comes AFTER the "MC Clinical sign" header, NOT the first Event date you see
569
635
  13. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
570
- 14. CRITICAL: For select fields, options like "No", "Non", "Non applicable", "Inconnu", "Unknown", "N/A" are VALID VALUES that can be selected. When the user says these words, treat them as legitimate option choices, NOT always as negations or refusals to answer. HOWEVER, for non-select fields (string, number, date, etc.), if the user says "I don't know", "unknown", "not known", "inconnu", "je ne sais pas", this usually means they have NO VALUE to provide, unless explicitely precised - leave the field empty, do NOT fill it with the literal text "unknown" or "I don't know"
636
+ 14. CRITICAL - UNKNOWN/NO/N-A VALUES: The interpretation of words like "Unknown", "Inconnu", "No", "Non", "N/A", "Non applicable" depends ENTIRELY on whether the field has those words as explicit options. Rule: if the user says "field X is Unknown" (or any similar phrasing) AND "Unknown" (or a close variant) exists in the options list for that field, then SELECT that option - it is a valid value. If the user says such words for a field that does NOT have them as options (string, number, date, etc.), then it means the user has no value to provide - leave the field empty, do NOT fill it with the literal text
637
+ 15. CRITICAL - PATTERN VALIDATION: Some fields have a regex pattern shown as "pattern=xxx". The value you extract MUST match this pattern exactly. If the transcription is too ambiguous to produce a value that matches the pattern, leave the field EMPTY
571
638
 
572
639
  Respond with JSON in this exact format: {"schema": {...}}`;
573
640
  let userPrompt = `
@@ -631,15 +698,138 @@ class OpenAILLMService {
631
698
  }
632
699
  }
633
700
  }
701
+ class AnthropicLLMService {
702
+ constructor(config) {
703
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
704
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
705
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'claude-sonnet-4.5';
706
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
707
+ if (!this.useProxy && !this.apiKey) {
708
+ throw new Error('Anthropic API key is required');
709
+ }
710
+ }
711
+ async fillFormFromTranscription(transcription, schema) {
712
+ return this.fillForm(transcription, schema);
713
+ }
714
+ async fillFormFromJson(json, schema) {
715
+ return this.fillForm(json, schema);
716
+ }
717
+ async fillForm(data, schema) {
718
+ var _a;
719
+ const endpoint = this.useProxy ? `${this.proxyUrl}/complete-anthropic` : 'https://api.anthropic.com/v1/messages';
720
+ const headers = { 'Content-Type': 'application/json' };
721
+ if (!this.useProxy) {
722
+ headers['x-api-key'] = this.apiKey;
723
+ headers['anthropic-version'] = '2023-06-01';
724
+ }
725
+ const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
726
+ const systemPrompt = this.buildSystemPrompt();
727
+ const userPrompt = `Data: "${data}"
728
+
729
+ Form fields:
730
+ ${JSON.stringify(finalSchema, null, 2)}
731
+
732
+ Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
733
+ const body = this.useProxy
734
+ ? { model: this.model, messages: [{ role: 'user', content: userPrompt }], system: systemPrompt }
735
+ : { model: this.model, max_tokens: 4096, system: systemPrompt, messages: [{ role: 'user', content: userPrompt }] };
736
+ const response = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
737
+ if (!response.ok) {
738
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
739
+ throw new Error(`Anthropic API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
740
+ }
741
+ const result = await response.json();
742
+ const content = this.useProxy ? result.choices[0].message.content : result.content[0].text;
743
+ return JSON.parse(content);
744
+ }
745
+ buildSystemPrompt() {
746
+ return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
747
+ Rules:
748
+ 1. Only extract values that can be confidently determined
749
+ 2. Respect field types (string, number, datetime, boolean, select)
750
+ 3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
751
+ 4. For date fields, use DD/MM/YYYY format
752
+ 5. For select fields, use exact option values from the provided choices
753
+ 6. Leave fields empty if no relevant information is found
754
+ 7. Fields marked readonly MUST NOT receive values
755
+ 8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
756
+ 9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
757
+ 10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
758
+ Respond ONLY with valid JSON.`;
759
+ }
760
+ }
761
+ class MistralLLMService {
762
+ constructor(config) {
763
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
764
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
765
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'mistral-medium-latest';
766
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
767
+ if (!this.useProxy && !this.apiKey) {
768
+ throw new Error('Mistral API key is required');
769
+ }
770
+ }
771
+ async fillFormFromTranscription(transcription, schema) {
772
+ return this.fillForm(transcription, schema);
773
+ }
774
+ async fillFormFromJson(json, schema) {
775
+ return this.fillForm(json, schema);
776
+ }
777
+ async fillForm(data, schema) {
778
+ var _a;
779
+ const endpoint = this.useProxy ? `${this.proxyUrl}/complete-mistral` : 'https://api.mistral.ai/v1/chat/completions';
780
+ const headers = { 'Content-Type': 'application/json' };
781
+ if (!this.useProxy) {
782
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
783
+ }
784
+ const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
785
+ const systemPrompt = this.buildSystemPrompt();
786
+ const userPrompt = `Data: "${data}"
787
+
788
+ Form fields:
789
+ ${JSON.stringify(finalSchema, null, 2)}
790
+
791
+ Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
792
+ const response = await fetch(endpoint, {
793
+ method: 'POST',
794
+ headers,
795
+ body: JSON.stringify({
796
+ model: this.model,
797
+ messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }],
798
+ response_format: { type: 'json_object' },
799
+ }),
800
+ });
801
+ if (!response.ok) {
802
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
803
+ throw new Error(`Mistral API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
804
+ }
805
+ const result = await response.json();
806
+ return JSON.parse(result.choices[0].message.content);
807
+ }
808
+ buildSystemPrompt() {
809
+ return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
810
+ Rules:
811
+ 1. Only extract values that can be confidently determined
812
+ 2. Respect field types (string, number, datetime, boolean, select)
813
+ 3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
814
+ 4. For date fields, use DD/MM/YYYY format
815
+ 5. For select fields, use exact option values from the provided choices
816
+ 6. Leave fields empty if no relevant information is found
817
+ 7. Fields marked readonly MUST NOT receive values
818
+ 8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
819
+ 9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
820
+ 10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
821
+ Respond ONLY with valid JSON.`;
822
+ }
823
+ }
634
824
  class LLMServiceFactory {
635
825
  static create(config) {
636
826
  var _a;
637
827
  const provider = ((_a = config.llm) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
638
828
  switch (provider) {
639
- case 'openai':
640
- return new OpenAILLMService(config.llm);
641
- default:
642
- throw new Error(`Unsupported LLM provider: ${provider}`);
829
+ case 'openai': return new OpenAILLMService(config.llm);
830
+ case 'anthropic': return new AnthropicLLMService(config.llm);
831
+ case 'mistral': return new MistralLLMService(config.llm);
832
+ default: throw new Error(`Unsupported LLM provider: ${provider}`);
643
833
  }
644
834
  }
645
835
  }
@@ -4159,6 +4349,8 @@ const VoiceFormRecorder = class {
4159
4349
  this.apiProxyUrl = 'http://localhost:8492';
4160
4350
  this.transcriptionModel = 'gpt-4o-transcribe';
4161
4351
  this.completionModel = 'gpt-5-mini';
4352
+ this.transcriptionProvider = 'openai';
4353
+ this.completionProvider = 'openai';
4162
4354
  this.context = undefined;
4163
4355
  this.classificationRootUrl = 'http://localhost';
4164
4356
  this.language = 'en';
@@ -4170,6 +4362,7 @@ const VoiceFormRecorder = class {
4170
4362
  this.isRecording = false;
4171
4363
  this.isProcessing = false;
4172
4364
  this.hasError = false;
4365
+ this.hasTransientError = false;
4173
4366
  this.transcription = '';
4174
4367
  this.filledData = null;
4175
4368
  this.debugInfo = {};
@@ -4182,7 +4375,21 @@ const VoiceFormRecorder = class {
4182
4375
  if (!input.files || input.files.length === 0)
4183
4376
  return;
4184
4377
  const file = input.files[0];
4185
- this.processAudioContent(file);
4378
+ const isEn = this.language === 'en';
4379
+ // Validate file type
4380
+ const validAudioTypes = /^audio\//;
4381
+ const validAudioExtensions = /\.(mp3|wav|webm|m4a|ogg|flac|aac|opus)$/i;
4382
+ if (!validAudioTypes.test(file.type) && !validAudioExtensions.test(file.name)) {
4383
+ this.hasTransientError = true;
4384
+ this.statusMessage = isEn
4385
+ ? `Invalid file type: "${file.name}". Please select an audio file.`
4386
+ : `Type de fichier invalide : "${file.name}". Veuillez sélectionner un fichier audio.`;
4387
+ input.value = '';
4388
+ return;
4389
+ }
4390
+ // Reset the input so the same file can be re-selected after an error
4391
+ input.value = '';
4392
+ await this.processAudioContent(file);
4186
4393
  };
4187
4394
  this.audioRecorder = new AudioRecorderService();
4188
4395
  }
@@ -4206,17 +4413,33 @@ const VoiceFormRecorder = class {
4206
4413
  this.updateDebugInfo(errorMessage, { error: errorMessage });
4207
4414
  }
4208
4415
  else {
4416
+ // Validate transcription provider/model
4417
+ const allowedTranscriptionModels = TRANSCRIPTION_MODELS[this.transcriptionProvider];
4418
+ if (!allowedTranscriptionModels) {
4419
+ throw new Error(`Unsupported transcription provider: '${this.transcriptionProvider}'. Allowed: openai, mistral`);
4420
+ }
4421
+ if (!allowedTranscriptionModels.includes(this.transcriptionModel)) {
4422
+ throw new Error(`Model '${this.transcriptionModel}' is not allowed for transcription provider '${this.transcriptionProvider}'. Allowed: ${allowedTranscriptionModels.join(', ')}`);
4423
+ }
4424
+ // Validate completion provider/model
4425
+ const allowedCompletionModels = COMPLETION_MODELS[this.completionProvider];
4426
+ if (!allowedCompletionModels) {
4427
+ throw new Error(`Unsupported completion provider: '${this.completionProvider}'. Allowed: openai, anthropic, mistral`);
4428
+ }
4429
+ if (!allowedCompletionModels.includes(this.completionModel)) {
4430
+ throw new Error(`Model '${this.completionModel}' is not allowed for completion provider '${this.completionProvider}'. Allowed: ${allowedCompletionModels.join(', ')}`);
4431
+ }
4209
4432
  // Parse form schema
4210
4433
  this.parsedSchema = JSON.parse(this.formJson || '{}');
4211
4434
  // Parse service configuration
4212
4435
  this.parsedConfig = JSON.parse(this.serviceConfig || '{}');
4213
4436
  // Add API key to config if provided via prop
4214
4437
  if (this.apiKey) {
4215
- this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { apiKey: this.apiKey, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { apiKey: this.apiKey, model: this.completionModel }) });
4438
+ this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { provider: this.transcriptionProvider, apiKey: this.apiKey, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { provider: this.completionProvider, apiKey: this.apiKey, model: this.completionModel }) });
4216
4439
  }
4217
4440
  else {
4218
4441
  // Use proxy API if no API key provided
4219
- this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { useProxy: true, proxyUrl: this.apiProxyUrl, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { useProxy: true, proxyUrl: this.apiProxyUrl, model: this.completionModel }) });
4442
+ this.parsedConfig = Object.assign(Object.assign({}, this.parsedConfig), { speechToText: Object.assign(Object.assign({}, this.parsedConfig.speechToText), { provider: this.transcriptionProvider, useProxy: true, proxyUrl: this.apiProxyUrl, model: this.transcriptionModel }), llm: Object.assign(Object.assign({}, this.parsedConfig.llm), { provider: this.completionProvider, useProxy: true, proxyUrl: this.apiProxyUrl, model: this.completionModel }) });
4220
4443
  }
4221
4444
  // Initialize services
4222
4445
  this.speechToTextService = SpeechToTextServiceFactory.create(this.parsedConfig);
@@ -4226,6 +4449,7 @@ const VoiceFormRecorder = class {
4226
4449
  config: this.parsedConfig
4227
4450
  });
4228
4451
  this.hasError = false;
4452
+ this.hasTransientError = false;
4229
4453
  this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.idle) || (this.language == 'en' ? 'Select an input method' : 'Sélectionner une méthode de saisie');
4230
4454
  if (this.parsedInputTypes.length === 0) {
4231
4455
  this.inputTypes = 'voice';
@@ -4246,6 +4470,91 @@ const VoiceFormRecorder = class {
4246
4470
  } });
4247
4471
  }
4248
4472
  }
4473
+ getMicErrorMessage(error) {
4474
+ var _a, _b;
4475
+ const isEn = this.language === 'en';
4476
+ const errorName = error.name || '';
4477
+ if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
4478
+ return isEn
4479
+ ? 'Microphone access denied. Allow microphone access in your browser settings and try again.'
4480
+ : "Accès au microphone refusé. Autorisez l'accès au microphone dans les paramètres du navigateur et réessayez.";
4481
+ }
4482
+ if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
4483
+ return isEn
4484
+ ? 'No microphone found. Please connect a microphone and try again.'
4485
+ : 'Aucun microphone trouvé. Connectez un microphone et réessayez.';
4486
+ }
4487
+ if (errorName === 'NotReadableError' || errorName === 'TrackStartError') {
4488
+ return isEn
4489
+ ? 'Microphone is busy or unavailable. Close other apps using the microphone and try again.'
4490
+ : 'Le microphone est occupé ou indisponible. Fermez les autres applications utilisant le microphone et réessayez.';
4491
+ }
4492
+ if (errorName === 'AbortError') {
4493
+ return isEn
4494
+ ? 'Microphone access was interrupted. Please try again.'
4495
+ : "L'accès au microphone a été interrompu. Veuillez réessayer.";
4496
+ }
4497
+ if (errorName === 'OverconstrainedError') {
4498
+ return isEn
4499
+ ? 'Microphone does not support the required audio settings.'
4500
+ : 'Le microphone ne supporte pas les paramètres audio requis.';
4501
+ }
4502
+ if (((_a = error.message) === null || _a === void 0 ? void 0 : _a.includes('HTTPS')) || ((_b = error.message) === null || _b === void 0 ? void 0 : _b.includes('not supported'))) {
4503
+ return isEn
4504
+ ? 'Microphone access requires HTTPS or localhost.'
4505
+ : "L'accès au microphone nécessite HTTPS ou localhost.";
4506
+ }
4507
+ return isEn ? `Microphone error: ${error.message}` : `Erreur microphone : ${error.message}`;
4508
+ }
4509
+ getReadableErrorMessage(error, phase) {
4510
+ const isEn = this.language === 'en';
4511
+ const msg = error.message || '';
4512
+ if (/401|unauthorized|invalid.{0,20}(api )?key|authentication failed/i.test(msg)) {
4513
+ return isEn
4514
+ ? 'Authentication failed: invalid API key. Check your configuration.'
4515
+ : "Échec d'authentification : clé API invalide. Vérifiez votre configuration.";
4516
+ }
4517
+ if (/403|forbidden/i.test(msg)) {
4518
+ return isEn
4519
+ ? 'Access denied: check your API permissions.'
4520
+ : 'Accès refusé : vérifiez vos permissions API.';
4521
+ }
4522
+ if (/429|rate.{0,10}limit|too many requests/i.test(msg)) {
4523
+ return isEn
4524
+ ? 'Rate limit exceeded. Please wait a moment and try again.'
4525
+ : 'Limite de requêtes dépassée. Veuillez attendre quelques secondes et réessayer.';
4526
+ }
4527
+ if (/404|not found/i.test(msg)) {
4528
+ return isEn
4529
+ ? 'API endpoint not found. Check your proxy URL configuration.'
4530
+ : "Endpoint API introuvable. Vérifiez l'URL de votre proxy.";
4531
+ }
4532
+ if (/5\d\d|server error|internal server|bad gateway|service unavailable/i.test(msg)) {
4533
+ return isEn
4534
+ ? 'Server error. Please try again in a moment.'
4535
+ : 'Erreur serveur. Veuillez réessayer dans un instant.';
4536
+ }
4537
+ if (/network|failed to fetch|err_connection|connection refused/i.test(msg)) {
4538
+ return isEn
4539
+ ? 'Network error: check your connection and proxy server.'
4540
+ : 'Erreur réseau : vérifiez votre connexion et le serveur proxy.';
4541
+ }
4542
+ if (/json|parse error|unexpected token|invalid response/i.test(msg)) {
4543
+ return isEn
4544
+ ? 'Unexpected response from AI service. Please try again.'
4545
+ : 'Réponse inattendue du service IA. Veuillez réessayer.';
4546
+ }
4547
+ if (phase === 'transcription') {
4548
+ return isEn ? `Transcription failed: ${msg}` : `Échec de la transcription : ${msg}`;
4549
+ }
4550
+ if (phase === 'llm') {
4551
+ return isEn ? `Form filling failed: ${msg}` : `Échec du remplissage du formulaire : ${msg}`;
4552
+ }
4553
+ if (phase === 'ocr') {
4554
+ return isEn ? `OCR processing failed: ${msg}` : `Échec du traitement OCR : ${msg}`;
4555
+ }
4556
+ return isEn ? `Processing error: ${msg}` : `Erreur de traitement : ${msg}`;
4557
+ }
4249
4558
  async handleRecordClick() {
4250
4559
  if (this.isProcessing)
4251
4560
  return;
@@ -4259,7 +4568,7 @@ const VoiceFormRecorder = class {
4259
4568
  async startRecording() {
4260
4569
  var _a;
4261
4570
  try {
4262
- this.hasError = false;
4571
+ this.hasTransientError = false;
4263
4572
  this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
4264
4573
  this.updateDebugInfo('Start Recording Attempt', {});
4265
4574
  await this.audioRecorder.startRecording();
@@ -4272,8 +4581,8 @@ const VoiceFormRecorder = class {
4272
4581
  });
4273
4582
  }
4274
4583
  catch (error) {
4275
- this.hasError = true;
4276
- this.statusMessage = (this.language == 'en' ? `Recording failed: ${error.message}` : `Echec de l'enregistrement : ${error.message}`);
4584
+ this.hasTransientError = true;
4585
+ this.statusMessage = this.getMicErrorMessage(error);
4277
4586
  this.updateDebugInfo('Recording Error', { error: error.message });
4278
4587
  this.formFilled.emit({
4279
4588
  success: false,
@@ -4283,40 +4592,36 @@ const VoiceFormRecorder = class {
4283
4592
  }
4284
4593
  async processJsonForm(ocrData) {
4285
4594
  var _a, _b, _c;
4595
+ this.isProcessing = true;
4596
+ this.hasTransientError = false;
4597
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing document...' : 'Traitement du document ...');
4286
4598
  try {
4287
- this.isProcessing = true;
4288
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
4289
- // Extract content from OCR format
4290
4599
  const extractedData = (ocrData === null || ocrData === void 0 ? void 0 : ocrData.content) || ocrData;
4291
4600
  const jsonForm = JSON.stringify(extractedData);
4292
- // Fill form using LLM
4601
+ if (!extractedData) {
4602
+ throw Object.assign(new Error('OCR returned no data'), { phase: 'ocr' });
4603
+ }
4293
4604
  this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4294
4605
  const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4295
- const filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
4296
- // Extract filled data
4606
+ let filledSchema;
4607
+ try {
4608
+ filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
4609
+ }
4610
+ catch (llmError) {
4611
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
4612
+ }
4297
4613
  this.filledData = this.extractFilledData(filledSchema);
4298
- this.updateDebugInfo('Form Filled', {
4299
- filledSchema,
4300
- extractedData: this.filledData
4301
- });
4614
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
4302
4615
  this.parsedSchema = this.filledData;
4303
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
4304
- this.hasError = false;
4305
- // Emit success event
4306
- this.formFilled.emit({
4307
- success: true,
4308
- data: this.filledData,
4309
- jsonForm: jsonForm
4310
- });
4616
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
4617
+ this.formFilled.emit({ success: true, data: this.filledData, jsonForm });
4311
4618
  }
4312
4619
  catch (error) {
4313
- this.hasError = true;
4314
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
4315
- this.formFilled.emit({
4316
- success: false,
4317
- error: error.message,
4318
- jsonForm: JSON.stringify(ocrData)
4319
- });
4620
+ console.error('OCR processing error:', error);
4621
+ this.hasTransientError = true;
4622
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'llm');
4623
+ this.updateDebugInfo('OCR Processing Error', { phase: error.phase, error: error.message });
4624
+ this.formFilled.emit({ success: false, error: error.message, jsonForm: JSON.stringify(ocrData) });
4320
4625
  }
4321
4626
  finally {
4322
4627
  this.isProcessing = false;
@@ -4324,83 +4629,100 @@ const VoiceFormRecorder = class {
4324
4629
  }
4325
4630
  async processAudioContent(audioFile) {
4326
4631
  var _a, _b, _c;
4327
- this.updateDebugInfo('Audio Captured', {
4328
- size: audioFile.size,
4329
- type: audioFile.type
4330
- });
4632
+ this.isProcessing = true;
4633
+ this.hasTransientError = false;
4634
+ this.updateDebugInfo('Audio Captured', { size: audioFile.size, type: audioFile.type });
4331
4635
  try {
4332
- // Transcribe audio
4333
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
4334
- const transcription = await this.speechToTextService.transcribe(audioFile, this.language);
4335
- this.transcription = transcription;
4336
- this.updateDebugInfo('Transcription Complete', { transcription });
4636
+ // Validate audio file before sending to API
4637
+ const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB (API limit)
4638
+ if (audioFile.size <= 44) { // WAV header alone is 44 bytes; effectively empty
4639
+ this.hasTransientError = true;
4640
+ this.statusMessage = this.language == 'en'
4641
+ ? 'Recording is empty or too short. Please try again.'
4642
+ : "L'enregistrement est vide ou trop court. Veuillez réessayer.";
4643
+ this.formFilled.emit({ success: false, error: 'Empty or too short audio' });
4644
+ return;
4645
+ }
4646
+ if (audioFile.size > MAX_AUDIO_SIZE) {
4647
+ this.hasTransientError = true;
4648
+ this.statusMessage = this.language == 'en'
4649
+ ? `File too large (${Math.round(audioFile.size / 1024 / 1024)}MB). Maximum is 25MB.`
4650
+ : `Fichier trop volumineux (${Math.round(audioFile.size / 1024 / 1024)}Mo). Maximum : 25Mo.`;
4651
+ this.formFilled.emit({ success: false, error: 'Audio file too large' });
4652
+ return;
4653
+ }
4654
+ // Step 1: Transcription
4655
+ let transcription;
4656
+ try {
4657
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
4658
+ transcription = await this.speechToTextService.transcribe(audioFile, this.language);
4659
+ this.transcription = transcription;
4660
+ this.updateDebugInfo('Transcription Complete', { transcription });
4661
+ }
4662
+ catch (transcriptionError) {
4663
+ throw Object.assign(new Error(transcriptionError.message), { phase: 'transcription' });
4664
+ }
4337
4665
  if (!transcription.trim()) {
4338
- throw new Error('No speech detected in the recording');
4666
+ this.hasTransientError = true;
4667
+ this.statusMessage = this.language == 'en'
4668
+ ? 'No speech detected. Please speak clearly and try again.'
4669
+ : 'Aucune parole détectée. Parlez clairement et réessayez.';
4670
+ this.formFilled.emit({ success: false, error: 'No speech detected', transcription: '' });
4671
+ return;
4672
+ }
4673
+ // Step 2: LLM form filling
4674
+ let filledSchema;
4675
+ try {
4676
+ this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4677
+ const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4678
+ filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
4679
+ }
4680
+ catch (llmError) {
4681
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
4339
4682
  }
4340
- // Fill form using LLM
4341
- this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4342
- const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4343
- const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
4344
- // Extract filled data
4345
4683
  this.filledData = this.extractFilledData(filledSchema);
4346
- this.updateDebugInfo('Form Filled', {
4347
- filledSchema,
4348
- extractedData: this.filledData
4349
- });
4684
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
4350
4685
  this.parsedSchema = this.filledData;
4351
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
4352
- this.hasError = false;
4353
- // Emit success event
4354
- this.formFilled.emit({
4355
- success: true,
4356
- data: this.filledData,
4357
- transcription: transcription
4358
- });
4686
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
4687
+ this.formFilled.emit({ success: true, data: this.filledData, transcription });
4359
4688
  }
4360
4689
  catch (error) {
4361
- console.error('Transcription error:', error);
4362
- this.hasError = true;
4363
- this.statusMessage = (this.language == 'en' ? 'Audio processing failed.' : 'Échec du traitement audio.');
4364
- this.updateDebugInfo('Transcription Error', { error: error.message });
4365
- this.formFilled.emit({
4366
- success: false,
4367
- error: error.message,
4368
- transcription: this.transcription
4369
- });
4690
+ console.error('Audio processing error:', error);
4691
+ this.hasTransientError = true;
4692
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'audio-upload');
4693
+ this.updateDebugInfo('Audio Processing Error', { phase: error.phase, error: error.message });
4694
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
4695
+ }
4696
+ finally {
4697
+ this.isProcessing = false;
4370
4698
  }
4371
4699
  }
4372
4700
  async stopRecordingAndProcess() {
4373
4701
  var _a;
4702
+ this.isRecording = false;
4703
+ this.isProcessing = true;
4704
+ this.hasTransientError = false;
4705
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
4706
+ this.updateDebugInfo('Stop Recording', {});
4707
+ this.recordingStateChanged.emit({ isRecording: false, state: 'processing' });
4374
4708
  try {
4375
- this.isRecording = false;
4376
- this.isProcessing = true;
4377
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
4378
- this.updateDebugInfo('Stop Recording', {});
4379
- this.recordingStateChanged.emit({
4380
- isRecording: false,
4381
- state: 'processing'
4382
- });
4383
- // Stop recording and get audio blob
4384
4709
  const audioBlob = await this.audioRecorder.stopRecording();
4385
- const audioContent = new File([audioBlob], 'audio.webm', { type: 'audio/webm' });
4386
- this.processAudioContent(audioContent);
4710
+ const audioContent = new File([audioBlob], 'audio.wav', { type: 'audio/wav' });
4711
+ // processAudioContent manages isProcessing itself; await ensures we don't return early
4712
+ await this.processAudioContent(audioContent);
4387
4713
  }
4388
4714
  catch (error) {
4389
- this.hasError = true;
4390
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
4391
- this.updateDebugInfo('Processing Error', { error: error.message });
4392
- this.formFilled.emit({
4393
- success: false,
4394
- error: error.message,
4395
- transcription: this.transcription
4396
- });
4715
+ // Only reached if audioRecorder.stopRecording() itself throws
4716
+ this.hasTransientError = true;
4717
+ this.isProcessing = false;
4718
+ this.statusMessage = (this.language == 'en'
4719
+ ? `Failed to stop recording: ${error.message}`
4720
+ : `Impossible d'arrêter l'enregistrement : ${error.message}`);
4721
+ this.updateDebugInfo('Stop Recording Error', { error: error.message });
4722
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
4397
4723
  }
4398
4724
  finally {
4399
- this.isProcessing = false;
4400
- this.recordingStateChanged.emit({
4401
- isRecording: false,
4402
- state: 'idle'
4403
- });
4725
+ this.recordingStateChanged.emit({ isRecording: false, state: 'idle' });
4404
4726
  }
4405
4727
  }
4406
4728
  extractFilledData(filledData) {
@@ -4708,12 +5030,14 @@ const VoiceFormRecorder = class {
4708
5030
  return (h$1("button", { class: buttonClass, style: buttonStyle, onClick: () => this.handleRecordClick(), disabled: isDisabled, "aria-label": this.isRecording ? 'Stop recording' : 'Start recording' }, this.isProcessing ? (h$1("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h$1("circle", { cx: "12", cy: "12", r: "3" }, h$1("animate", { attributeName: "r", values: "3;6;3", dur: "1s", repeatCount: "indefinite" }), h$1("animate", { attributeName: "opacity", values: "1;0.3;1", dur: "1s", repeatCount: "indefinite" })))) : this.isRecording ? (h$1("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h$1("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }))) : (h$1("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h$1("circle", { cx: "12", cy: "12", r: "8" })))));
4709
5031
  }
4710
5032
  renderStatusMessage() {
5033
+ const hasAnyError = this.hasError || this.hasTransientError;
4711
5034
  const statusClass = [
4712
5035
  'status-text',
4713
- this.hasError && 'error',
4714
- this.filledData && !this.hasError && 'success'
5036
+ hasAnyError && 'error',
5037
+ this.filledData && !hasAnyError && 'success'
4715
5038
  ].filter(Boolean).join(' ');
4716
- return h$1("div", { class: statusClass }, this.statusMessage);
5039
+ const statusStyle = this.getStatusStyle();
5040
+ return h$1("div", { class: statusClass, style: statusStyle }, this.statusMessage);
4717
5041
  }
4718
5042
  renderFormPreview() {
4719
5043
  if (!this.parsedSchema)
@@ -4886,13 +5210,16 @@ const VoiceFormRecorder = class {
4886
5210
  }
4887
5211
  render() {
4888
5212
  const containerStyle = this.getContainerStyle();
4889
- const statusStyle = this.getStatusStyle();
4890
- return (h$1("div", { key: '8503450dbd4de2cb91172bd2bdbdf16a0c4cdce0' }, h$1("div", { key: '8b3f553574a64754312f68f2abe0d64f57c56d3e', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h$1("div", { key: '3d08a37d611bc91dbe11c065c633f0d36c8b362e', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? h$1("div", { class: "status-text", style: statusStyle }, this.statusMessage) : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
5213
+ return (h$1("div", { key: 'd2eb6b74840a57809920b3dbba3fd7dfbeef8531' }, h$1("div", { key: 'e66e1d4fc23176f9024ca20413535440a8be3ce8', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h$1("div", { key: '57dd7b882771d1f0bd56a97bfd0a06ffee09428e', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? this.renderStatusMessage() : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
4891
5214
  }
4892
5215
  static get watchers() { return {
4893
5216
  "formJson": ["initializeServices"],
4894
5217
  "serviceConfig": ["initializeServices"],
4895
- "theme": ["initializeServices"]
5218
+ "theme": ["initializeServices"],
5219
+ "transcriptionProvider": ["initializeServices"],
5220
+ "completionProvider": ["initializeServices"],
5221
+ "transcriptionModel": ["initializeServices"],
5222
+ "completionModel": ["initializeServices"]
4896
5223
  }; }
4897
5224
  };
4898
5225
  VoiceFormRecorder.style = voiceInputModuleCss;