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,69 +1,96 @@
1
1
  import { proxyCustomElement, HTMLElement as HTMLElement$1, createEvent, h as h$1 } from '@stencil/core/internal/client';
2
2
  import { d as defineCustomElement$2 } from './ocr-file-uploader2.js';
3
3
 
4
+ const TRANSCRIPTION_MODELS = {
5
+ openai: ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe', 'whisper-1'],
6
+ mistral: ['voxtral-mini-latest', 'voxtral-mini-transcribe-realtime-latest'],
7
+ };
8
+ const COMPLETION_MODELS = {
9
+ 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'],
10
+ 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'],
11
+ mistral: ['mistral-large-latest', 'mistral-medium-latest', 'mistral-small-latest', 'ministral', 'mistral-nemo'],
12
+ };
13
+
4
14
  class AudioRecorderService {
5
15
  constructor() {
6
16
  this.mediaRecorder = null;
7
17
  this.audioChunks = [];
8
18
  this.stream = null;
19
+ this.audioContext = null;
20
+ this.scriptProcessor = null;
21
+ this.pcmChunks = [];
22
+ this.sampleRate = 16000;
9
23
  }
10
24
  async startRecording() {
11
- try {
12
- // Check if the API exists before calling
13
- if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
14
- console.error('Failed to start recording:', 'Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
15
- return; // Exit gracefully instead of throwing
16
- }
17
- this.stream = await navigator.mediaDevices.getUserMedia({
18
- audio: {
19
- echoCancellation: true,
20
- noiseSuppression: true,
21
- autoGainControl: true
22
- }
23
- });
24
- this.audioChunks = [];
25
- this.mediaRecorder = new MediaRecorder(this.stream, {
26
- mimeType: 'audio/webm;codecs=opus'
27
- });
28
- this.mediaRecorder.ondataavailable = (event) => {
29
- if (event.data && event.data.size > 0) {
30
- this.audioChunks.push(event.data);
31
- }
32
- };
33
- this.mediaRecorder.start(100); // Collect data every 100ms
34
- }
35
- catch (error) {
36
- console.error('Failed to start recording:', error);
25
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
26
+ throw new Error('Microphone access is not supported in this browser or the page is not served over HTTPS/localhost.');
37
27
  }
28
+ this.stream = await navigator.mediaDevices.getUserMedia({
29
+ audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
30
+ });
31
+ this.audioContext = new AudioContext({ sampleRate: this.sampleRate });
32
+ const source = this.audioContext.createMediaStreamSource(this.stream);
33
+ this.scriptProcessor = this.audioContext.createScriptProcessor(4096, 1, 1);
34
+ this.pcmChunks = [];
35
+ this.scriptProcessor.onaudioprocess = (e) => {
36
+ const input = e.inputBuffer.getChannelData(0);
37
+ this.pcmChunks.push(new Float32Array(input));
38
+ };
39
+ source.connect(this.scriptProcessor);
40
+ this.scriptProcessor.connect(this.audioContext.destination);
38
41
  }
39
42
  async stopRecording() {
40
- return new Promise((resolve, reject) => {
41
- if (!this.mediaRecorder) {
42
- reject(new Error('No active recording found'));
43
- return;
43
+ if (!this.audioContext || !this.scriptProcessor) {
44
+ throw new Error('No active recording found');
45
+ }
46
+ this.scriptProcessor.disconnect();
47
+ await this.audioContext.close();
48
+ const wavBlob = this.encodeWav(this.pcmChunks, this.sampleRate);
49
+ this.cleanup();
50
+ return wavBlob;
51
+ }
52
+ encodeWav(chunks, sampleRate) {
53
+ const totalSamples = chunks.reduce((acc, c) => acc + c.length, 0);
54
+ const buffer = new ArrayBuffer(44 + totalSamples * 2);
55
+ const view = new DataView(buffer);
56
+ const writeStr = (offset, str) => {
57
+ for (let i = 0; i < str.length; i++)
58
+ view.setUint8(offset + i, str.charCodeAt(i));
59
+ };
60
+ writeStr(0, 'RIFF');
61
+ view.setUint32(4, 36 + totalSamples * 2, true);
62
+ writeStr(8, 'WAVE');
63
+ writeStr(12, 'fmt ');
64
+ view.setUint32(16, 16, true); // PCM chunk size
65
+ view.setUint16(20, 1, true); // PCM format
66
+ view.setUint16(22, 1, true); // mono
67
+ view.setUint32(24, sampleRate, true);
68
+ view.setUint32(28, sampleRate * 2, true); // byte rate
69
+ view.setUint16(32, 2, true); // block align
70
+ view.setUint16(34, 16, true); // bits per sample
71
+ writeStr(36, 'data');
72
+ view.setUint32(40, totalSamples * 2, true);
73
+ let offset = 44;
74
+ for (const chunk of chunks) {
75
+ for (let i = 0; i < chunk.length; i++) {
76
+ const s = Math.max(-1, Math.min(1, chunk[i]));
77
+ view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
78
+ offset += 2;
44
79
  }
45
- this.mediaRecorder.onstop = () => {
46
- const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
47
- this.cleanup();
48
- resolve(audioBlob);
49
- };
50
- this.mediaRecorder.onerror = (event) => {
51
- reject(new Error(`Recording error: ${event}`));
52
- };
53
- this.mediaRecorder.stop();
54
- });
80
+ }
81
+ return new Blob([buffer], { type: 'audio/wav' });
55
82
  }
56
83
  isRecording() {
57
- var _a;
58
- return ((_a = this.mediaRecorder) === null || _a === void 0 ? void 0 : _a.state) === 'recording';
84
+ return this.scriptProcessor !== null;
59
85
  }
60
86
  cleanup() {
61
87
  if (this.stream) {
62
88
  this.stream.getTracks().forEach(track => track.stop());
63
89
  this.stream = null;
64
90
  }
65
- this.mediaRecorder = null;
66
- this.audioChunks = [];
91
+ this.audioContext = null;
92
+ this.scriptProcessor = null;
93
+ this.pcmChunks = [];
67
94
  }
68
95
  }
69
96
 
@@ -125,15 +152,49 @@ class WhisperSpeechToTextService {
125
152
  }
126
153
  }
127
154
  }
155
+ class MistralSpeechToTextService {
156
+ constructor(config) {
157
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
158
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
159
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'voxtral-mini-latest';
160
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
161
+ if (!this.useProxy && !this.apiKey) {
162
+ throw new Error('Mistral API key is required for speech-to-text service');
163
+ }
164
+ }
165
+ async transcribe(audioContent, lang = 'en') {
166
+ var _a;
167
+ try {
168
+ const formData = new FormData();
169
+ formData.append('file', audioContent);
170
+ formData.append('model', this.model);
171
+ formData.append('language', lang);
172
+ const endpoint = this.useProxy ? `${this.proxyUrl}/transcribe-mistral` : 'https://api.mistral.ai/v1/audio/transcriptions';
173
+ const headers = {};
174
+ if (!this.useProxy) {
175
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
176
+ }
177
+ const response = await fetch(endpoint, { method: 'POST', headers, body: formData });
178
+ if (!response.ok) {
179
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
180
+ throw new Error(`Transcription failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
181
+ }
182
+ const result = await response.json();
183
+ return result.text || '';
184
+ }
185
+ catch (error) {
186
+ throw new Error(`Mistral speech-to-text failed: ${error.message}`);
187
+ }
188
+ }
189
+ }
128
190
  class SpeechToTextServiceFactory {
129
191
  static create(config) {
130
192
  var _a;
131
- const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || 'whisper';
193
+ const provider = ((_a = config.speechToText) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
132
194
  switch (provider) {
133
- case 'whisper':
134
- return new WhisperSpeechToTextService(config.speechToText);
135
- default:
136
- throw new Error(`Unsupported speech-to-text provider: ${provider}`);
195
+ case 'openai': return new WhisperSpeechToTextService(config.speechToText);
196
+ case 'mistral': return new MistralSpeechToTextService(config.speechToText);
197
+ default: throw new Error(`Unsupported speech-to-text provider: ${provider}`);
137
198
  }
138
199
  }
139
200
  }
@@ -325,7 +386,8 @@ class OpenAILLMService {
325
386
  `${field.readonly ? ', readonly' : ''}` +
326
387
  `${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
327
388
  `${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
328
- `${field.max && field.max !== "" ? ', max=' + field.max : ''}`;
389
+ `${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
390
+ `${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
329
391
  if (field.options && field.options.length > 0) {
330
392
  if (field.options.length < 50) {
331
393
  description += `) - options: ${field.options.join(', ')}`;
@@ -347,7 +409,8 @@ class OpenAILLMService {
347
409
  `${field.readonly ? ', readonly' : ''}` +
348
410
  `${field.default !== undefined && field.default !== null && field.default !== '' ? ', default=' + field.default : ''}` +
349
411
  `${field.min && field.min !== "" ? ', min=' + field.min : ''}` +
350
- `${field.max && field.max !== "" ? ', max=' + field.max : ''}`;
412
+ `${field.max && field.max !== "" ? ', max=' + field.max : ''}` +
413
+ `${field.pattern && field.pattern !== "" ? ', pattern=' + field.pattern : ''}`;
351
414
  if (field.options && field.options.length > 0) {
352
415
  if (field.options.length < 50) {
353
416
  description += `) - options: ${field.options.join(', ')}`;
@@ -491,12 +554,14 @@ class OpenAILLMService {
491
554
  7. Only include fields where relevant information is found
492
555
  8. The current GMT datetime is ${new Date().toGMTString()}
493
556
  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
557
+ 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
494
558
  10. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
495
559
  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
496
560
  12. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
497
- 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"
561
+ 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
498
562
  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
499
563
  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
564
+ 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
500
565
 
501
566
  Respond with JSON in this exact format: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
502
567
  let userPrompt = `
@@ -565,10 +630,12 @@ class OpenAILLMService {
565
630
  8. Return the same schema structure with 'default' values filled
566
631
  9. The current GMT datetime is ${new Date().toGMTString()}
567
632
  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
633
+ 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
568
634
  11. IMPORTANT: Fields marked as "readonly" are presentation elements (headers, labels) that provide context but MUST NOT receive values
569
635
  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
570
636
  13. Use readonly fields as contextual hints to understand form structure and field grouping, but never fill them
571
- 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"
637
+ 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
638
+ 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
572
639
 
573
640
  Respond with JSON in this exact format: {"schema": {...}}`;
574
641
  let userPrompt = `
@@ -632,15 +699,138 @@ class OpenAILLMService {
632
699
  }
633
700
  }
634
701
  }
702
+ class AnthropicLLMService {
703
+ constructor(config) {
704
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
705
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
706
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'claude-sonnet-4.5';
707
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
708
+ if (!this.useProxy && !this.apiKey) {
709
+ throw new Error('Anthropic API key is required');
710
+ }
711
+ }
712
+ async fillFormFromTranscription(transcription, schema) {
713
+ return this.fillForm(transcription, schema);
714
+ }
715
+ async fillFormFromJson(json, schema) {
716
+ return this.fillForm(json, schema);
717
+ }
718
+ async fillForm(data, schema) {
719
+ var _a;
720
+ const endpoint = this.useProxy ? `${this.proxyUrl}/complete-anthropic` : 'https://api.anthropic.com/v1/messages';
721
+ const headers = { 'Content-Type': 'application/json' };
722
+ if (!this.useProxy) {
723
+ headers['x-api-key'] = this.apiKey;
724
+ headers['anthropic-version'] = '2023-06-01';
725
+ }
726
+ const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
727
+ const systemPrompt = this.buildSystemPrompt();
728
+ const userPrompt = `Data: "${data}"
729
+
730
+ Form fields:
731
+ ${JSON.stringify(finalSchema, null, 2)}
732
+
733
+ Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
734
+ const body = this.useProxy
735
+ ? { model: this.model, messages: [{ role: 'user', content: userPrompt }], system: systemPrompt }
736
+ : { model: this.model, max_tokens: 4096, system: systemPrompt, messages: [{ role: 'user', content: userPrompt }] };
737
+ const response = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
738
+ if (!response.ok) {
739
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
740
+ throw new Error(`Anthropic API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
741
+ }
742
+ const result = await response.json();
743
+ const content = this.useProxy ? result.choices[0].message.content : result.content[0].text;
744
+ return JSON.parse(content);
745
+ }
746
+ buildSystemPrompt() {
747
+ return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
748
+ Rules:
749
+ 1. Only extract values that can be confidently determined
750
+ 2. Respect field types (string, number, datetime, boolean, select)
751
+ 3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
752
+ 4. For date fields, use DD/MM/YYYY format
753
+ 5. For select fields, use exact option values from the provided choices
754
+ 6. Leave fields empty if no relevant information is found
755
+ 7. Fields marked readonly MUST NOT receive values
756
+ 8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
757
+ 9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
758
+ 10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
759
+ Respond ONLY with valid JSON.`;
760
+ }
761
+ }
762
+ class MistralLLMService {
763
+ constructor(config) {
764
+ this.useProxy = (config === null || config === void 0 ? void 0 : config.useProxy) || false;
765
+ this.proxyUrl = (config === null || config === void 0 ? void 0 : config.proxyUrl) || 'http://localhost:8492';
766
+ this.model = (config === null || config === void 0 ? void 0 : config.model) || 'mistral-medium-latest';
767
+ this.apiKey = this.useProxy ? '' : ((config === null || config === void 0 ? void 0 : config.apiKey) || '');
768
+ if (!this.useProxy && !this.apiKey) {
769
+ throw new Error('Mistral API key is required');
770
+ }
771
+ }
772
+ async fillFormFromTranscription(transcription, schema) {
773
+ return this.fillForm(transcription, schema);
774
+ }
775
+ async fillFormFromJson(json, schema) {
776
+ return this.fillForm(json, schema);
777
+ }
778
+ async fillForm(data, schema) {
779
+ var _a;
780
+ const endpoint = this.useProxy ? `${this.proxyUrl}/complete-mistral` : 'https://api.mistral.ai/v1/chat/completions';
781
+ const headers = { 'Content-Type': 'application/json' };
782
+ if (!this.useProxy) {
783
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
784
+ }
785
+ const finalSchema = (schema === null || schema === void 0 ? void 0 : schema.fields) || (schema === null || schema === void 0 ? void 0 : schema.schema) || schema;
786
+ const systemPrompt = this.buildSystemPrompt();
787
+ const userPrompt = `Data: "${data}"
788
+
789
+ Form fields:
790
+ ${JSON.stringify(finalSchema, null, 2)}
791
+
792
+ Respond with JSON: {"fields": [{"id": "field_id", "name": "field_name", "value": "extracted_value"}]}`;
793
+ const response = await fetch(endpoint, {
794
+ method: 'POST',
795
+ headers,
796
+ body: JSON.stringify({
797
+ model: this.model,
798
+ messages: [{ role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }],
799
+ response_format: { type: 'json_object' },
800
+ }),
801
+ });
802
+ if (!response.ok) {
803
+ const err = await response.json().catch(() => ({ error: 'Unknown error' }));
804
+ throw new Error(`Mistral API failed: ${((_a = err.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText}`);
805
+ }
806
+ const result = await response.json();
807
+ return JSON.parse(result.choices[0].message.content);
808
+ }
809
+ buildSystemPrompt() {
810
+ return `You are an expert form-filling assistant. Extract values from the input data and fill form fields.
811
+ Rules:
812
+ 1. Only extract values that can be confidently determined
813
+ 2. Respect field types (string, number, datetime, boolean, select)
814
+ 3. For datetime fields, use ISO format (YYYY-MM-DDTHH:MM)
815
+ 4. For date fields, use DD/MM/YYYY format
816
+ 5. For select fields, use exact option values from the provided choices
817
+ 6. Leave fields empty if no relevant information is found
818
+ 7. Fields marked readonly MUST NOT receive values
819
+ 8. DATE/DATETIME OUT OF RANGE: if a date falls outside min/max constraints, leave the field EMPTY
820
+ 9. PATTERN VALIDATION: if a field has a pattern, the value MUST match it exactly, otherwise leave empty
821
+ 10. UNKNOWN VALUES: if the user says "Unknown" and it exists as an option, select it; otherwise leave empty
822
+ Respond ONLY with valid JSON.`;
823
+ }
824
+ }
635
825
  class LLMServiceFactory {
636
826
  static create(config) {
637
827
  var _a;
638
828
  const provider = ((_a = config.llm) === null || _a === void 0 ? void 0 : _a.provider) || 'openai';
639
829
  switch (provider) {
640
- case 'openai':
641
- return new OpenAILLMService(config.llm);
642
- default:
643
- throw new Error(`Unsupported LLM provider: ${provider}`);
830
+ case 'openai': return new OpenAILLMService(config.llm);
831
+ case 'anthropic': return new AnthropicLLMService(config.llm);
832
+ case 'mistral': return new MistralLLMService(config.llm);
833
+ default: throw new Error(`Unsupported LLM provider: ${provider}`);
644
834
  }
645
835
  }
646
836
  }
@@ -4162,6 +4352,8 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4162
4352
  this.apiProxyUrl = 'http://localhost:8492';
4163
4353
  this.transcriptionModel = 'gpt-4o-transcribe';
4164
4354
  this.completionModel = 'gpt-5-mini';
4355
+ this.transcriptionProvider = 'openai';
4356
+ this.completionProvider = 'openai';
4165
4357
  this.context = undefined;
4166
4358
  this.classificationRootUrl = 'http://localhost';
4167
4359
  this.language = 'en';
@@ -4173,6 +4365,7 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4173
4365
  this.isRecording = false;
4174
4366
  this.isProcessing = false;
4175
4367
  this.hasError = false;
4368
+ this.hasTransientError = false;
4176
4369
  this.transcription = '';
4177
4370
  this.filledData = null;
4178
4371
  this.debugInfo = {};
@@ -4185,7 +4378,21 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4185
4378
  if (!input.files || input.files.length === 0)
4186
4379
  return;
4187
4380
  const file = input.files[0];
4188
- this.processAudioContent(file);
4381
+ const isEn = this.language === 'en';
4382
+ // Validate file type
4383
+ const validAudioTypes = /^audio\//;
4384
+ const validAudioExtensions = /\.(mp3|wav|webm|m4a|ogg|flac|aac|opus)$/i;
4385
+ if (!validAudioTypes.test(file.type) && !validAudioExtensions.test(file.name)) {
4386
+ this.hasTransientError = true;
4387
+ this.statusMessage = isEn
4388
+ ? `Invalid file type: "${file.name}". Please select an audio file.`
4389
+ : `Type de fichier invalide : "${file.name}". Veuillez sélectionner un fichier audio.`;
4390
+ input.value = '';
4391
+ return;
4392
+ }
4393
+ // Reset the input so the same file can be re-selected after an error
4394
+ input.value = '';
4395
+ await this.processAudioContent(file);
4189
4396
  };
4190
4397
  this.audioRecorder = new AudioRecorderService();
4191
4398
  }
@@ -4209,17 +4416,33 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4209
4416
  this.updateDebugInfo(errorMessage, { error: errorMessage });
4210
4417
  }
4211
4418
  else {
4419
+ // Validate transcription provider/model
4420
+ const allowedTranscriptionModels = TRANSCRIPTION_MODELS[this.transcriptionProvider];
4421
+ if (!allowedTranscriptionModels) {
4422
+ throw new Error(`Unsupported transcription provider: '${this.transcriptionProvider}'. Allowed: openai, mistral`);
4423
+ }
4424
+ if (!allowedTranscriptionModels.includes(this.transcriptionModel)) {
4425
+ throw new Error(`Model '${this.transcriptionModel}' is not allowed for transcription provider '${this.transcriptionProvider}'. Allowed: ${allowedTranscriptionModels.join(', ')}`);
4426
+ }
4427
+ // Validate completion provider/model
4428
+ const allowedCompletionModels = COMPLETION_MODELS[this.completionProvider];
4429
+ if (!allowedCompletionModels) {
4430
+ throw new Error(`Unsupported completion provider: '${this.completionProvider}'. Allowed: openai, anthropic, mistral`);
4431
+ }
4432
+ if (!allowedCompletionModels.includes(this.completionModel)) {
4433
+ throw new Error(`Model '${this.completionModel}' is not allowed for completion provider '${this.completionProvider}'. Allowed: ${allowedCompletionModels.join(', ')}`);
4434
+ }
4212
4435
  // Parse form schema
4213
4436
  this.parsedSchema = JSON.parse(this.formJson || '{}');
4214
4437
  // Parse service configuration
4215
4438
  this.parsedConfig = JSON.parse(this.serviceConfig || '{}');
4216
4439
  // Add API key to config if provided via prop
4217
4440
  if (this.apiKey) {
4218
- 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 }) });
4441
+ 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 }) });
4219
4442
  }
4220
4443
  else {
4221
4444
  // Use proxy API if no API key provided
4222
- 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 }) });
4445
+ 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 }) });
4223
4446
  }
4224
4447
  // Initialize services
4225
4448
  this.speechToTextService = SpeechToTextServiceFactory.create(this.parsedConfig);
@@ -4229,6 +4452,7 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4229
4452
  config: this.parsedConfig
4230
4453
  });
4231
4454
  this.hasError = false;
4455
+ this.hasTransientError = false;
4232
4456
  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');
4233
4457
  if (this.parsedInputTypes.length === 0) {
4234
4458
  this.inputTypes = 'voice';
@@ -4249,6 +4473,91 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4249
4473
  } });
4250
4474
  }
4251
4475
  }
4476
+ getMicErrorMessage(error) {
4477
+ var _a, _b;
4478
+ const isEn = this.language === 'en';
4479
+ const errorName = error.name || '';
4480
+ if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
4481
+ return isEn
4482
+ ? 'Microphone access denied. Allow microphone access in your browser settings and try again.'
4483
+ : "Accès au microphone refusé. Autorisez l'accès au microphone dans les paramètres du navigateur et réessayez.";
4484
+ }
4485
+ if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
4486
+ return isEn
4487
+ ? 'No microphone found. Please connect a microphone and try again.'
4488
+ : 'Aucun microphone trouvé. Connectez un microphone et réessayez.';
4489
+ }
4490
+ if (errorName === 'NotReadableError' || errorName === 'TrackStartError') {
4491
+ return isEn
4492
+ ? 'Microphone is busy or unavailable. Close other apps using the microphone and try again.'
4493
+ : 'Le microphone est occupé ou indisponible. Fermez les autres applications utilisant le microphone et réessayez.';
4494
+ }
4495
+ if (errorName === 'AbortError') {
4496
+ return isEn
4497
+ ? 'Microphone access was interrupted. Please try again.'
4498
+ : "L'accès au microphone a été interrompu. Veuillez réessayer.";
4499
+ }
4500
+ if (errorName === 'OverconstrainedError') {
4501
+ return isEn
4502
+ ? 'Microphone does not support the required audio settings.'
4503
+ : 'Le microphone ne supporte pas les paramètres audio requis.';
4504
+ }
4505
+ 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'))) {
4506
+ return isEn
4507
+ ? 'Microphone access requires HTTPS or localhost.'
4508
+ : "L'accès au microphone nécessite HTTPS ou localhost.";
4509
+ }
4510
+ return isEn ? `Microphone error: ${error.message}` : `Erreur microphone : ${error.message}`;
4511
+ }
4512
+ getReadableErrorMessage(error, phase) {
4513
+ const isEn = this.language === 'en';
4514
+ const msg = error.message || '';
4515
+ if (/401|unauthorized|invalid.{0,20}(api )?key|authentication failed/i.test(msg)) {
4516
+ return isEn
4517
+ ? 'Authentication failed: invalid API key. Check your configuration.'
4518
+ : "Échec d'authentification : clé API invalide. Vérifiez votre configuration.";
4519
+ }
4520
+ if (/403|forbidden/i.test(msg)) {
4521
+ return isEn
4522
+ ? 'Access denied: check your API permissions.'
4523
+ : 'Accès refusé : vérifiez vos permissions API.';
4524
+ }
4525
+ if (/429|rate.{0,10}limit|too many requests/i.test(msg)) {
4526
+ return isEn
4527
+ ? 'Rate limit exceeded. Please wait a moment and try again.'
4528
+ : 'Limite de requêtes dépassée. Veuillez attendre quelques secondes et réessayer.';
4529
+ }
4530
+ if (/404|not found/i.test(msg)) {
4531
+ return isEn
4532
+ ? 'API endpoint not found. Check your proxy URL configuration.'
4533
+ : "Endpoint API introuvable. Vérifiez l'URL de votre proxy.";
4534
+ }
4535
+ if (/5\d\d|server error|internal server|bad gateway|service unavailable/i.test(msg)) {
4536
+ return isEn
4537
+ ? 'Server error. Please try again in a moment.'
4538
+ : 'Erreur serveur. Veuillez réessayer dans un instant.';
4539
+ }
4540
+ if (/network|failed to fetch|err_connection|connection refused/i.test(msg)) {
4541
+ return isEn
4542
+ ? 'Network error: check your connection and proxy server.'
4543
+ : 'Erreur réseau : vérifiez votre connexion et le serveur proxy.';
4544
+ }
4545
+ if (/json|parse error|unexpected token|invalid response/i.test(msg)) {
4546
+ return isEn
4547
+ ? 'Unexpected response from AI service. Please try again.'
4548
+ : 'Réponse inattendue du service IA. Veuillez réessayer.';
4549
+ }
4550
+ if (phase === 'transcription') {
4551
+ return isEn ? `Transcription failed: ${msg}` : `Échec de la transcription : ${msg}`;
4552
+ }
4553
+ if (phase === 'llm') {
4554
+ return isEn ? `Form filling failed: ${msg}` : `Échec du remplissage du formulaire : ${msg}`;
4555
+ }
4556
+ if (phase === 'ocr') {
4557
+ return isEn ? `OCR processing failed: ${msg}` : `Échec du traitement OCR : ${msg}`;
4558
+ }
4559
+ return isEn ? `Processing error: ${msg}` : `Erreur de traitement : ${msg}`;
4560
+ }
4252
4561
  async handleRecordClick() {
4253
4562
  if (this.isProcessing)
4254
4563
  return;
@@ -4262,7 +4571,7 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4262
4571
  async startRecording() {
4263
4572
  var _a;
4264
4573
  try {
4265
- this.hasError = false;
4574
+ this.hasTransientError = false;
4266
4575
  this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
4267
4576
  this.updateDebugInfo('Start Recording Attempt', {});
4268
4577
  await this.audioRecorder.startRecording();
@@ -4275,8 +4584,8 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4275
4584
  });
4276
4585
  }
4277
4586
  catch (error) {
4278
- this.hasError = true;
4279
- this.statusMessage = (this.language == 'en' ? `Recording failed: ${error.message}` : `Echec de l'enregistrement : ${error.message}`);
4587
+ this.hasTransientError = true;
4588
+ this.statusMessage = this.getMicErrorMessage(error);
4280
4589
  this.updateDebugInfo('Recording Error', { error: error.message });
4281
4590
  this.formFilled.emit({
4282
4591
  success: false,
@@ -4286,40 +4595,36 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4286
4595
  }
4287
4596
  async processJsonForm(ocrData) {
4288
4597
  var _a, _b, _c;
4598
+ this.isProcessing = true;
4599
+ this.hasTransientError = false;
4600
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing document...' : 'Traitement du document ...');
4289
4601
  try {
4290
- this.isProcessing = true;
4291
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
4292
- // Extract content from OCR format
4293
4602
  const extractedData = (ocrData === null || ocrData === void 0 ? void 0 : ocrData.content) || ocrData;
4294
4603
  const jsonForm = JSON.stringify(extractedData);
4295
- // Fill form using LLM
4604
+ if (!extractedData) {
4605
+ throw Object.assign(new Error('OCR returned no data'), { phase: 'ocr' });
4606
+ }
4296
4607
  this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4297
4608
  const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4298
- const filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
4299
- // Extract filled data
4609
+ let filledSchema;
4610
+ try {
4611
+ filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
4612
+ }
4613
+ catch (llmError) {
4614
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
4615
+ }
4300
4616
  this.filledData = this.extractFilledData(filledSchema);
4301
- this.updateDebugInfo('Form Filled', {
4302
- filledSchema,
4303
- extractedData: this.filledData
4304
- });
4617
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
4305
4618
  this.parsedSchema = this.filledData;
4306
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
4307
- this.hasError = false;
4308
- // Emit success event
4309
- this.formFilled.emit({
4310
- success: true,
4311
- data: this.filledData,
4312
- jsonForm: jsonForm
4313
- });
4619
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
4620
+ this.formFilled.emit({ success: true, data: this.filledData, jsonForm });
4314
4621
  }
4315
4622
  catch (error) {
4316
- this.hasError = true;
4317
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
4318
- this.formFilled.emit({
4319
- success: false,
4320
- error: error.message,
4321
- jsonForm: JSON.stringify(ocrData)
4322
- });
4623
+ console.error('OCR processing error:', error);
4624
+ this.hasTransientError = true;
4625
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'llm');
4626
+ this.updateDebugInfo('OCR Processing Error', { phase: error.phase, error: error.message });
4627
+ this.formFilled.emit({ success: false, error: error.message, jsonForm: JSON.stringify(ocrData) });
4323
4628
  }
4324
4629
  finally {
4325
4630
  this.isProcessing = false;
@@ -4327,83 +4632,100 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4327
4632
  }
4328
4633
  async processAudioContent(audioFile) {
4329
4634
  var _a, _b, _c;
4330
- this.updateDebugInfo('Audio Captured', {
4331
- size: audioFile.size,
4332
- type: audioFile.type
4333
- });
4635
+ this.isProcessing = true;
4636
+ this.hasTransientError = false;
4637
+ this.updateDebugInfo('Audio Captured', { size: audioFile.size, type: audioFile.type });
4334
4638
  try {
4335
- // Transcribe audio
4336
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
4337
- const transcription = await this.speechToTextService.transcribe(audioFile, this.language);
4338
- this.transcription = transcription;
4339
- this.updateDebugInfo('Transcription Complete', { transcription });
4639
+ // Validate audio file before sending to API
4640
+ const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB (API limit)
4641
+ if (audioFile.size <= 44) { // WAV header alone is 44 bytes; effectively empty
4642
+ this.hasTransientError = true;
4643
+ this.statusMessage = this.language == 'en'
4644
+ ? 'Recording is empty or too short. Please try again.'
4645
+ : "L'enregistrement est vide ou trop court. Veuillez réessayer.";
4646
+ this.formFilled.emit({ success: false, error: 'Empty or too short audio' });
4647
+ return;
4648
+ }
4649
+ if (audioFile.size > MAX_AUDIO_SIZE) {
4650
+ this.hasTransientError = true;
4651
+ this.statusMessage = this.language == 'en'
4652
+ ? `File too large (${Math.round(audioFile.size / 1024 / 1024)}MB). Maximum is 25MB.`
4653
+ : `Fichier trop volumineux (${Math.round(audioFile.size / 1024 / 1024)}Mo). Maximum : 25Mo.`;
4654
+ this.formFilled.emit({ success: false, error: 'Audio file too large' });
4655
+ return;
4656
+ }
4657
+ // Step 1: Transcription
4658
+ let transcription;
4659
+ try {
4660
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
4661
+ transcription = await this.speechToTextService.transcribe(audioFile, this.language);
4662
+ this.transcription = transcription;
4663
+ this.updateDebugInfo('Transcription Complete', { transcription });
4664
+ }
4665
+ catch (transcriptionError) {
4666
+ throw Object.assign(new Error(transcriptionError.message), { phase: 'transcription' });
4667
+ }
4340
4668
  if (!transcription.trim()) {
4341
- throw new Error('No speech detected in the recording');
4669
+ this.hasTransientError = true;
4670
+ this.statusMessage = this.language == 'en'
4671
+ ? 'No speech detected. Please speak clearly and try again.'
4672
+ : 'Aucune parole détectée. Parlez clairement et réessayez.';
4673
+ this.formFilled.emit({ success: false, error: 'No speech detected', transcription: '' });
4674
+ return;
4675
+ }
4676
+ // Step 2: LLM form filling
4677
+ let filledSchema;
4678
+ try {
4679
+ this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4680
+ const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4681
+ filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
4682
+ }
4683
+ catch (llmError) {
4684
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
4342
4685
  }
4343
- // Fill form using LLM
4344
- this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
4345
- const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
4346
- const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
4347
- // Extract filled data
4348
4686
  this.filledData = this.extractFilledData(filledSchema);
4349
- this.updateDebugInfo('Form Filled', {
4350
- filledSchema,
4351
- extractedData: this.filledData
4352
- });
4687
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
4353
4688
  this.parsedSchema = this.filledData;
4354
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
4355
- this.hasError = false;
4356
- // Emit success event
4357
- this.formFilled.emit({
4358
- success: true,
4359
- data: this.filledData,
4360
- transcription: transcription
4361
- });
4689
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
4690
+ this.formFilled.emit({ success: true, data: this.filledData, transcription });
4362
4691
  }
4363
4692
  catch (error) {
4364
- console.error('Transcription error:', error);
4365
- this.hasError = true;
4366
- this.statusMessage = (this.language == 'en' ? 'Audio processing failed.' : 'Échec du traitement audio.');
4367
- this.updateDebugInfo('Transcription Error', { error: error.message });
4368
- this.formFilled.emit({
4369
- success: false,
4370
- error: error.message,
4371
- transcription: this.transcription
4372
- });
4693
+ console.error('Audio processing error:', error);
4694
+ this.hasTransientError = true;
4695
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'audio-upload');
4696
+ this.updateDebugInfo('Audio Processing Error', { phase: error.phase, error: error.message });
4697
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
4698
+ }
4699
+ finally {
4700
+ this.isProcessing = false;
4373
4701
  }
4374
4702
  }
4375
4703
  async stopRecordingAndProcess() {
4376
4704
  var _a;
4705
+ this.isRecording = false;
4706
+ this.isProcessing = true;
4707
+ this.hasTransientError = false;
4708
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
4709
+ this.updateDebugInfo('Stop Recording', {});
4710
+ this.recordingStateChanged.emit({ isRecording: false, state: 'processing' });
4377
4711
  try {
4378
- this.isRecording = false;
4379
- this.isProcessing = true;
4380
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
4381
- this.updateDebugInfo('Stop Recording', {});
4382
- this.recordingStateChanged.emit({
4383
- isRecording: false,
4384
- state: 'processing'
4385
- });
4386
- // Stop recording and get audio blob
4387
4712
  const audioBlob = await this.audioRecorder.stopRecording();
4388
- const audioContent = new File([audioBlob], 'audio.webm', { type: 'audio/webm' });
4389
- this.processAudioContent(audioContent);
4713
+ const audioContent = new File([audioBlob], 'audio.wav', { type: 'audio/wav' });
4714
+ // processAudioContent manages isProcessing itself; await ensures we don't return early
4715
+ await this.processAudioContent(audioContent);
4390
4716
  }
4391
4717
  catch (error) {
4392
- this.hasError = true;
4393
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
4394
- this.updateDebugInfo('Processing Error', { error: error.message });
4395
- this.formFilled.emit({
4396
- success: false,
4397
- error: error.message,
4398
- transcription: this.transcription
4399
- });
4718
+ // Only reached if audioRecorder.stopRecording() itself throws
4719
+ this.hasTransientError = true;
4720
+ this.isProcessing = false;
4721
+ this.statusMessage = (this.language == 'en'
4722
+ ? `Failed to stop recording: ${error.message}`
4723
+ : `Impossible d'arrêter l'enregistrement : ${error.message}`);
4724
+ this.updateDebugInfo('Stop Recording Error', { error: error.message });
4725
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
4400
4726
  }
4401
4727
  finally {
4402
- this.isProcessing = false;
4403
- this.recordingStateChanged.emit({
4404
- isRecording: false,
4405
- state: 'idle'
4406
- });
4728
+ this.recordingStateChanged.emit({ isRecording: false, state: 'idle' });
4407
4729
  }
4408
4730
  }
4409
4731
  extractFilledData(filledData) {
@@ -4711,12 +5033,14 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4711
5033
  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" })))));
4712
5034
  }
4713
5035
  renderStatusMessage() {
5036
+ const hasAnyError = this.hasError || this.hasTransientError;
4714
5037
  const statusClass = [
4715
5038
  'status-text',
4716
- this.hasError && 'error',
4717
- this.filledData && !this.hasError && 'success'
5039
+ hasAnyError && 'error',
5040
+ this.filledData && !hasAnyError && 'success'
4718
5041
  ].filter(Boolean).join(' ');
4719
- return h$1("div", { class: statusClass }, this.statusMessage);
5042
+ const statusStyle = this.getStatusStyle();
5043
+ return h$1("div", { class: statusClass, style: statusStyle }, this.statusMessage);
4720
5044
  }
4721
5045
  renderFormPreview() {
4722
5046
  if (!this.parsedSchema)
@@ -4889,13 +5213,16 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4889
5213
  }
4890
5214
  render() {
4891
5215
  const containerStyle = this.getContainerStyle();
4892
- const statusStyle = this.getStatusStyle();
4893
- 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() : "")));
5216
+ 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() : "")));
4894
5217
  }
4895
5218
  static get watchers() { return {
4896
5219
  "formJson": ["initializeServices"],
4897
5220
  "serviceConfig": ["initializeServices"],
4898
- "theme": ["initializeServices"]
5221
+ "theme": ["initializeServices"],
5222
+ "transcriptionProvider": ["initializeServices"],
5223
+ "completionProvider": ["initializeServices"],
5224
+ "transcriptionModel": ["initializeServices"],
5225
+ "completionModel": ["initializeServices"]
4899
5226
  }; }
4900
5227
  static get style() { return voiceInputModuleCss; }
4901
5228
  }, [257, "voice-input-module", {
@@ -4905,6 +5232,8 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4905
5232
  "apiProxyUrl": [1, "api-proxy-url"],
4906
5233
  "transcriptionModel": [1, "transcription-model"],
4907
5234
  "completionModel": [1, "completion-model"],
5235
+ "transcriptionProvider": [1, "transcription-provider"],
5236
+ "completionProvider": [1, "completion-provider"],
4908
5237
  "context": [1],
4909
5238
  "classificationRootUrl": [1, "classification-root-url"],
4910
5239
  "language": [1],
@@ -4917,6 +5246,7 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4917
5246
  "isProcessing": [32],
4918
5247
  "statusMessage": [32],
4919
5248
  "hasError": [32],
5249
+ "hasTransientError": [32],
4920
5250
  "transcription": [32],
4921
5251
  "filledData": [32],
4922
5252
  "debugInfo": [32],
@@ -4928,7 +5258,11 @@ const VoiceFormRecorder = /*@__PURE__*/ proxyCustomElement(class VoiceFormRecord
4928
5258
  }, undefined, {
4929
5259
  "formJson": ["initializeServices"],
4930
5260
  "serviceConfig": ["initializeServices"],
4931
- "theme": ["initializeServices"]
5261
+ "theme": ["initializeServices"],
5262
+ "transcriptionProvider": ["initializeServices"],
5263
+ "completionProvider": ["initializeServices"],
5264
+ "transcriptionModel": ["initializeServices"],
5265
+ "completionModel": ["initializeServices"]
4932
5266
  }]);
4933
5267
  function defineCustomElement$1() {
4934
5268
  if (typeof customElements === "undefined") {