reneco-advanced-input-module 0.0.22 → 0.0.24

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 (28) hide show
  1. package/dist/cjs/loader.cjs.js +1 -1
  2. package/dist/cjs/voice-input-module.cjs.entry.js +237 -96
  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.css +34 -0
  7. package/dist/collection/components/voice-input-module/voice-input-module.js +239 -96
  8. package/dist/collection/components/voice-input-module/voice-input-module.js.map +1 -1
  9. package/dist/components/voice-input-module.js +239 -96
  10. package/dist/components/voice-input-module.js.map +1 -1
  11. package/dist/esm/loader.js +1 -1
  12. package/dist/esm/voice-input-module.entry.js +237 -96
  13. package/dist/esm/voice-input-module.entry.js.map +1 -1
  14. package/dist/esm/voice-input-module.js +1 -1
  15. package/dist/types/components/voice-input-module/voice-input-module.d.ts +6 -0
  16. package/dist/voice-input-module/{p-0e2b9ca0.entry.js → p-b3bf4feb.entry.js} +3 -3
  17. package/dist/voice-input-module/p-b3bf4feb.entry.js.map +1 -0
  18. package/dist/voice-input-module/voice-input-module.entry.esm.js.map +1 -1
  19. package/dist/voice-input-module/voice-input-module.esm.js +1 -1
  20. package/package.json +1 -1
  21. package/www/build/{p-812b92c7.js → p-3aeb495d.js} +1 -1
  22. package/www/build/{p-0e2b9ca0.entry.js → p-b3bf4feb.entry.js} +3 -3
  23. package/www/build/p-b3bf4feb.entry.js.map +1 -0
  24. package/www/build/voice-input-module.entry.esm.js.map +1 -1
  25. package/www/build/voice-input-module.esm.js +1 -1
  26. package/www/index.html +1 -1
  27. package/dist/voice-input-module/p-0e2b9ca0.entry.js.map +0 -1
  28. package/www/build/p-0e2b9ca0.entry.js.map +0 -1
@@ -341,6 +341,40 @@ input[type="date"].form-input {
341
341
  user-select: none;
342
342
  }
343
343
 
344
+ /* Skeleton loading state */
345
+ @keyframes skeleton-shimmer {
346
+ 0% { background-position: -200% 0; }
347
+ 100% { background-position: 200% 0; }
348
+ }
349
+
350
+ .skeleton-circle {
351
+ border-radius: 50%;
352
+ flex-shrink: 0;
353
+ background: linear-gradient(
354
+ 90deg,
355
+ #e2e8f0 25%,
356
+ #f8fafc 50%,
357
+ #e2e8f0 75%
358
+ );
359
+ background-size: 200% 100%;
360
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
361
+ }
362
+
363
+ .skeleton-status {
364
+ height: 14px;
365
+ border-radius: 7px;
366
+ width: 140px;
367
+ background: linear-gradient(
368
+ 90deg,
369
+ #e2e8f0 25%,
370
+ #f8fafc 50%,
371
+ #e2e8f0 75%
372
+ );
373
+ background-size: 200% 100%;
374
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
375
+ animation-delay: 0.15s;
376
+ }
377
+
344
378
  /* Force OCR component to respect size */
345
379
  ocr-file-uploader {
346
380
  display: block;
@@ -37,10 +37,12 @@ export class VoiceFormRecorder {
37
37
  this.isRecording = false;
38
38
  this.isProcessing = false;
39
39
  this.hasError = false;
40
+ this.hasTransientError = false;
40
41
  this.transcription = '';
41
42
  this.filledData = null;
42
43
  this.debugInfo = {};
43
44
  this.isReadonlyMode = true; // Start in readonly preview mode
45
+ this.isInitializing = true;
44
46
  this.triggerAudioRecordUpload = () => {
45
47
  this.fileInputAudioRecord.click();
46
48
  };
@@ -48,14 +50,35 @@ export class VoiceFormRecorder {
48
50
  const input = event.target;
49
51
  if (!input.files || input.files.length === 0)
50
52
  return;
53
+ if (this.isProcessing || this.isRecording) {
54
+ input.value = '';
55
+ return;
56
+ }
51
57
  const file = input.files[0];
52
- this.processAudioContent(file);
58
+ const isEn = this.language === 'en';
59
+ // Validate file type
60
+ const validAudioTypes = /^audio\//;
61
+ const validAudioExtensions = /\.(mp3|wav|webm|m4a|ogg|flac|aac|opus)$/i;
62
+ if (!validAudioTypes.test(file.type) && !validAudioExtensions.test(file.name)) {
63
+ this.hasTransientError = true;
64
+ this.statusMessage = isEn
65
+ ? `Invalid file type: "${file.name}". Please select an audio file.`
66
+ : `Type de fichier invalide : "${file.name}". Veuillez sélectionner un fichier audio.`;
67
+ input.value = '';
68
+ return;
69
+ }
70
+ // Reset the input so the same file can be re-selected after an error
71
+ input.value = '';
72
+ await this.processAudioContent(file);
53
73
  };
54
74
  this.audioRecorder = new AudioRecorderService();
55
75
  }
56
76
  componentWillLoad() {
57
77
  this.initializeServices();
58
78
  }
79
+ componentDidLoad() {
80
+ this.isInitializing = false;
81
+ }
59
82
  initializeServices() {
60
83
  var _a;
61
84
  // Parse theme
@@ -109,6 +132,7 @@ export class VoiceFormRecorder {
109
132
  config: this.parsedConfig
110
133
  });
111
134
  this.hasError = false;
135
+ this.hasTransientError = false;
112
136
  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');
113
137
  if (this.parsedInputTypes.length === 0) {
114
138
  this.inputTypes = 'voice';
@@ -129,6 +153,91 @@ export class VoiceFormRecorder {
129
153
  } });
130
154
  }
131
155
  }
156
+ getMicErrorMessage(error) {
157
+ var _a, _b;
158
+ const isEn = this.language === 'en';
159
+ const errorName = error.name || '';
160
+ if (errorName === 'NotAllowedError' || errorName === 'PermissionDeniedError') {
161
+ return isEn
162
+ ? 'Microphone access denied. Allow microphone access in your browser settings and try again.'
163
+ : "Accès au microphone refusé. Autorisez l'accès au microphone dans les paramètres du navigateur et réessayez.";
164
+ }
165
+ if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
166
+ return isEn
167
+ ? 'No microphone found. Please connect a microphone and try again.'
168
+ : 'Aucun microphone trouvé. Connectez un microphone et réessayez.';
169
+ }
170
+ if (errorName === 'NotReadableError' || errorName === 'TrackStartError') {
171
+ return isEn
172
+ ? 'Microphone is busy or unavailable. Close other apps using the microphone and try again.'
173
+ : 'Le microphone est occupé ou indisponible. Fermez les autres applications utilisant le microphone et réessayez.';
174
+ }
175
+ if (errorName === 'AbortError') {
176
+ return isEn
177
+ ? 'Microphone access was interrupted. Please try again.'
178
+ : "L'accès au microphone a été interrompu. Veuillez réessayer.";
179
+ }
180
+ if (errorName === 'OverconstrainedError') {
181
+ return isEn
182
+ ? 'Microphone does not support the required audio settings.'
183
+ : 'Le microphone ne supporte pas les paramètres audio requis.';
184
+ }
185
+ 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'))) {
186
+ return isEn
187
+ ? 'Microphone access requires HTTPS or localhost.'
188
+ : "L'accès au microphone nécessite HTTPS ou localhost.";
189
+ }
190
+ return isEn ? `Microphone error: ${error.message}` : `Erreur microphone : ${error.message}`;
191
+ }
192
+ getReadableErrorMessage(error, phase) {
193
+ const isEn = this.language === 'en';
194
+ const msg = error.message || '';
195
+ if (/401|unauthorized|invalid.{0,20}(api )?key|authentication failed/i.test(msg)) {
196
+ return isEn
197
+ ? 'Authentication failed: invalid API key. Check your configuration.'
198
+ : "Échec d'authentification : clé API invalide. Vérifiez votre configuration.";
199
+ }
200
+ if (/403|forbidden/i.test(msg)) {
201
+ return isEn
202
+ ? 'Access denied: check your API permissions.'
203
+ : 'Accès refusé : vérifiez vos permissions API.';
204
+ }
205
+ if (/429|rate.{0,10}limit|too many requests/i.test(msg)) {
206
+ return isEn
207
+ ? 'Rate limit exceeded. Please wait a moment and try again.'
208
+ : 'Limite de requêtes dépassée. Veuillez attendre quelques secondes et réessayer.';
209
+ }
210
+ if (/404|not found/i.test(msg)) {
211
+ return isEn
212
+ ? 'API endpoint not found. Check your proxy URL configuration.'
213
+ : "Endpoint API introuvable. Vérifiez l'URL de votre proxy.";
214
+ }
215
+ if (/5\d\d|server error|internal server|bad gateway|service unavailable/i.test(msg)) {
216
+ return isEn
217
+ ? 'Server error. Please try again in a moment.'
218
+ : 'Erreur serveur. Veuillez réessayer dans un instant.';
219
+ }
220
+ if (/network|failed to fetch|err_connection|connection refused/i.test(msg)) {
221
+ return isEn
222
+ ? 'Network error: check your connection and proxy server.'
223
+ : 'Erreur réseau : vérifiez votre connexion et le serveur proxy.';
224
+ }
225
+ if (/json|parse error|unexpected token|invalid response/i.test(msg)) {
226
+ return isEn
227
+ ? 'Unexpected response from AI service. Please try again.'
228
+ : 'Réponse inattendue du service IA. Veuillez réessayer.';
229
+ }
230
+ if (phase === 'transcription') {
231
+ return isEn ? `Transcription failed: ${msg}` : `Échec de la transcription : ${msg}`;
232
+ }
233
+ if (phase === 'llm') {
234
+ return isEn ? `Form filling failed: ${msg}` : `Échec du remplissage du formulaire : ${msg}`;
235
+ }
236
+ if (phase === 'ocr') {
237
+ return isEn ? `OCR processing failed: ${msg}` : `Échec du traitement OCR : ${msg}`;
238
+ }
239
+ return isEn ? `Processing error: ${msg}` : `Erreur de traitement : ${msg}`;
240
+ }
132
241
  async handleRecordClick() {
133
242
  if (this.isProcessing)
134
243
  return;
@@ -142,7 +251,7 @@ export class VoiceFormRecorder {
142
251
  async startRecording() {
143
252
  var _a;
144
253
  try {
145
- this.hasError = false;
254
+ this.hasTransientError = false;
146
255
  this.statusMessage = (this.language == 'en' ? 'Starting recording...' : `Enregistrement ...`);
147
256
  this.updateDebugInfo('Start Recording Attempt', {});
148
257
  await this.audioRecorder.startRecording();
@@ -155,8 +264,8 @@ export class VoiceFormRecorder {
155
264
  });
156
265
  }
157
266
  catch (error) {
158
- this.hasError = true;
159
- this.statusMessage = (this.language == 'en' ? `Recording failed: ${error.message}` : `Echec de l'enregistrement : ${error.message}`);
267
+ this.hasTransientError = true;
268
+ this.statusMessage = this.getMicErrorMessage(error);
160
269
  this.updateDebugInfo('Recording Error', { error: error.message });
161
270
  this.formFilled.emit({
162
271
  success: false,
@@ -166,40 +275,38 @@ export class VoiceFormRecorder {
166
275
  }
167
276
  async processJsonForm(ocrData) {
168
277
  var _a, _b, _c;
278
+ if (this.isProcessing || this.isRecording)
279
+ return;
280
+ this.isProcessing = true;
281
+ this.hasTransientError = false;
282
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing document...' : 'Traitement du document ...');
169
283
  try {
170
- this.isProcessing = true;
171
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing json...' : `Traitement du json ...`);
172
- // Extract content from OCR format
173
284
  const extractedData = (ocrData === null || ocrData === void 0 ? void 0 : ocrData.content) || ocrData;
174
285
  const jsonForm = JSON.stringify(extractedData);
175
- // Fill form using LLM
286
+ if (!extractedData) {
287
+ throw Object.assign(new Error('OCR returned no data'), { phase: 'ocr' });
288
+ }
176
289
  this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
177
290
  const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
178
- const filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
179
- // Extract filled data
291
+ let filledSchema;
292
+ try {
293
+ filledSchema = await this.llmService.fillFormFromJson(jsonForm, trimmedSchema);
294
+ }
295
+ catch (llmError) {
296
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
297
+ }
180
298
  this.filledData = this.extractFilledData(filledSchema);
181
- this.updateDebugInfo('Form Filled', {
182
- filledSchema,
183
- extractedData: this.filledData
184
- });
299
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
185
300
  this.parsedSchema = this.filledData;
186
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
187
- this.hasError = false;
188
- // Emit success event
189
- this.formFilled.emit({
190
- success: true,
191
- data: this.filledData,
192
- jsonForm: jsonForm
193
- });
301
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
302
+ this.formFilled.emit({ success: true, data: this.filledData, jsonForm });
194
303
  }
195
304
  catch (error) {
196
- this.hasError = true;
197
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
198
- this.formFilled.emit({
199
- success: false,
200
- error: error.message,
201
- jsonForm: JSON.stringify(ocrData)
202
- });
305
+ console.error('OCR processing error:', error);
306
+ this.hasTransientError = true;
307
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'llm');
308
+ this.updateDebugInfo('OCR Processing Error', { phase: error.phase, error: error.message });
309
+ this.formFilled.emit({ success: false, error: error.message, jsonForm: JSON.stringify(ocrData) });
203
310
  }
204
311
  finally {
205
312
  this.isProcessing = false;
@@ -207,83 +314,102 @@ export class VoiceFormRecorder {
207
314
  }
208
315
  async processAudioContent(audioFile) {
209
316
  var _a, _b, _c;
210
- this.updateDebugInfo('Audio Captured', {
211
- size: audioFile.size,
212
- type: audioFile.type
213
- });
317
+ if (this.isProcessing || this.isRecording)
318
+ return;
319
+ this.isProcessing = true;
320
+ this.hasTransientError = false;
321
+ this.updateDebugInfo('Audio Captured', { size: audioFile.size, type: audioFile.type });
214
322
  try {
215
- // Transcribe audio
216
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
217
- const transcription = await this.speechToTextService.transcribe(audioFile, this.language);
218
- this.transcription = transcription;
219
- this.updateDebugInfo('Transcription Complete', { transcription });
323
+ // Validate audio file before sending to API
324
+ const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB (API limit)
325
+ if (audioFile.size <= 44) { // WAV header alone is 44 bytes; effectively empty
326
+ this.hasTransientError = true;
327
+ this.statusMessage = this.language == 'en'
328
+ ? 'Recording is empty or too short. Please try again.'
329
+ : "L'enregistrement est vide ou trop court. Veuillez réessayer.";
330
+ this.formFilled.emit({ success: false, error: 'Empty or too short audio' });
331
+ return;
332
+ }
333
+ if (audioFile.size > MAX_AUDIO_SIZE) {
334
+ this.hasTransientError = true;
335
+ this.statusMessage = this.language == 'en'
336
+ ? `File too large (${Math.round(audioFile.size / 1024 / 1024)}MB). Maximum is 25MB.`
337
+ : `Fichier trop volumineux (${Math.round(audioFile.size / 1024 / 1024)}Mo). Maximum : 25Mo.`;
338
+ this.formFilled.emit({ success: false, error: 'Audio file too large' });
339
+ return;
340
+ }
341
+ // Step 1: Transcription
342
+ let transcription;
343
+ try {
344
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.transcribing) || (this.language == 'en' ? 'Transcribing speech...' : 'Transcription du texte ...');
345
+ transcription = await this.speechToTextService.transcribe(audioFile, this.language);
346
+ this.transcription = transcription;
347
+ this.updateDebugInfo('Transcription Complete', { transcription });
348
+ }
349
+ catch (transcriptionError) {
350
+ throw Object.assign(new Error(transcriptionError.message), { phase: 'transcription' });
351
+ }
220
352
  if (!transcription.trim()) {
221
- throw new Error('No speech detected in the recording');
353
+ this.hasTransientError = true;
354
+ this.statusMessage = this.language == 'en'
355
+ ? 'No speech detected. Please speak clearly and try again.'
356
+ : 'Aucune parole détectée. Parlez clairement et réessayez.';
357
+ this.formFilled.emit({ success: false, error: 'No speech detected', transcription: '' });
358
+ return;
359
+ }
360
+ // Step 2: LLM form filling
361
+ let filledSchema;
362
+ try {
363
+ this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
364
+ const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
365
+ filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
366
+ }
367
+ catch (llmError) {
368
+ throw Object.assign(new Error(llmError.message), { phase: 'llm' });
222
369
  }
223
- // Fill form using LLM
224
- this.statusMessage = ((_b = this.parsedTheme.texts) === null || _b === void 0 ? void 0 : _b.filling) || (this.language == 'en' ? 'Filling form fields...' : 'Remplissage du formulaire ...');
225
- const trimmedSchema = await this.trimSchemaForAI(this.parsedSchema);
226
- const filledSchema = await this.llmService.fillFormFromTranscription(transcription, trimmedSchema);
227
- // Extract filled data
228
370
  this.filledData = this.extractFilledData(filledSchema);
229
- this.updateDebugInfo('Form Filled', {
230
- filledSchema,
231
- extractedData: this.filledData
232
- });
371
+ this.updateDebugInfo('Form Filled', { filledSchema, extractedData: this.filledData });
233
372
  this.parsedSchema = this.filledData;
234
- this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire remplis !');
235
- this.hasError = false;
236
- // Emit success event
237
- this.formFilled.emit({
238
- success: true,
239
- data: this.filledData,
240
- transcription: transcription
241
- });
373
+ this.statusMessage = ((_c = this.parsedTheme.texts) === null || _c === void 0 ? void 0 : _c.completed) || (this.language == 'en' ? 'Form completed!' : 'Formulaire rempli !');
374
+ this.formFilled.emit({ success: true, data: this.filledData, transcription });
242
375
  }
243
376
  catch (error) {
244
- console.error('Transcription error:', error);
245
- this.hasError = true;
246
- this.statusMessage = (this.language == 'en' ? 'Audio processing failed.' : 'Échec du traitement audio.');
247
- this.updateDebugInfo('Transcription Error', { error: error.message });
248
- this.formFilled.emit({
249
- success: false,
250
- error: error.message,
251
- transcription: this.transcription
252
- });
377
+ console.error('Audio processing error:', error);
378
+ this.hasTransientError = true;
379
+ this.statusMessage = this.getReadableErrorMessage(error, error.phase || 'audio-upload');
380
+ this.updateDebugInfo('Audio Processing Error', { phase: error.phase, error: error.message });
381
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
382
+ }
383
+ finally {
384
+ this.isProcessing = false;
253
385
  }
254
386
  }
255
387
  async stopRecordingAndProcess() {
256
388
  var _a;
389
+ this.isRecording = false;
390
+ this.isProcessing = true;
391
+ this.hasTransientError = false;
392
+ this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
393
+ this.updateDebugInfo('Stop Recording', {});
394
+ this.recordingStateChanged.emit({ isRecording: false, state: 'processing' });
257
395
  try {
258
- this.isRecording = false;
259
- this.isProcessing = true;
260
- this.statusMessage = ((_a = this.parsedTheme.texts) === null || _a === void 0 ? void 0 : _a.processing) || (this.language == 'en' ? 'Processing audio...' : `Traitement de l'audio ...`);
261
- this.updateDebugInfo('Stop Recording', {});
262
- this.recordingStateChanged.emit({
263
- isRecording: false,
264
- state: 'processing'
265
- });
266
- // Stop recording and get audio blob
267
396
  const audioBlob = await this.audioRecorder.stopRecording();
268
397
  const audioContent = new File([audioBlob], 'audio.wav', { type: 'audio/wav' });
269
- this.processAudioContent(audioContent);
398
+ // processAudioContent manages isProcessing itself; await ensures we don't return early
399
+ await this.processAudioContent(audioContent);
270
400
  }
271
401
  catch (error) {
272
- this.hasError = true;
273
- this.statusMessage = (this.language == 'en' ? `Processing failed: ${error.message}` : `Erreur de traitement : ${error.message}`);
274
- this.updateDebugInfo('Processing Error', { error: error.message });
275
- this.formFilled.emit({
276
- success: false,
277
- error: error.message,
278
- transcription: this.transcription
279
- });
402
+ // Only reached if audioRecorder.stopRecording() itself throws
403
+ this.hasTransientError = true;
404
+ this.isProcessing = false;
405
+ this.statusMessage = (this.language == 'en'
406
+ ? `Failed to stop recording: ${error.message}`
407
+ : `Impossible d'arrêter l'enregistrement : ${error.message}`);
408
+ this.updateDebugInfo('Stop Recording Error', { error: error.message });
409
+ this.formFilled.emit({ success: false, error: error.message, transcription: this.transcription });
280
410
  }
281
411
  finally {
282
- this.isProcessing = false;
283
- this.recordingStateChanged.emit({
284
- isRecording: false,
285
- state: 'idle'
286
- });
412
+ this.recordingStateChanged.emit({ isRecording: false, state: 'idle' });
287
413
  }
288
414
  }
289
415
  extractFilledData(filledData) {
@@ -508,7 +634,7 @@ export class VoiceFormRecorder {
508
634
  textColor: ocrStyle.textColor || '#ffffff'
509
635
  }
510
636
  };
511
- const isDisabled = this.isProcessing || this.hasError;
637
+ const isDisabled = this.isProcessing || this.hasError || this.isRecording;
512
638
  return (h("div", { style: { width: size, height: size, flexShrink: '0', opacity: isDisabled ? '0.6' : '1', pointerEvents: isDisabled ? 'none' : 'auto' } }, h("ocr-file-uploader", { batch: false, "json-schema": this.convertSchemaToOcrFormat(this.parsedSchema), callback: (data) => { this.processJsonForm(data); }, theme: JSON.stringify(ocrTheme), style: { width: '100%', height: '100%' } })));
513
639
  }
514
640
  convertSchemaToOcrFormat(schema) {
@@ -563,7 +689,7 @@ export class VoiceFormRecorder {
563
689
  return;
564
690
  const audioStyle = ((_a = this.parsedTheme.buttons) === null || _a === void 0 ? void 0 : _a.audio) || {};
565
691
  const size = audioStyle.size || '50px';
566
- const isDisabled = this.isProcessing || this.hasError;
692
+ const isDisabled = this.isProcessing || this.hasError || this.isRecording;
567
693
  const buttonStyle = {
568
694
  stroke: audioStyle.backgroundColor || '#3b82f6',
569
695
  fill: audioStyle.backgroundColor || '#3b82f6',
@@ -592,13 +718,26 @@ export class VoiceFormRecorder {
592
718
  };
593
719
  return (h("button", { class: buttonClass, style: buttonStyle, onClick: () => this.handleRecordClick(), disabled: isDisabled, "aria-label": this.isRecording ? 'Stop recording' : 'Start recording' }, this.isProcessing ? (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("circle", { cx: "12", cy: "12", r: "3" }, h("animate", { attributeName: "r", values: "3;6;3", dur: "1s", repeatCount: "indefinite" }), h("animate", { attributeName: "opacity", values: "1;0.3;1", dur: "1s", repeatCount: "indefinite" })))) : this.isRecording ? (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("rect", { x: "6", y: "6", width: "12", height: "12", rx: "2" }))) : (h("svg", { class: "record-icon", viewBox: "0 0 24 24" }, h("circle", { cx: "12", cy: "12", r: "8" })))));
594
720
  }
721
+ renderLoadingSkeleton() {
722
+ var _a;
723
+ const types = this.parsedInputTypes.length > 0 ? this.parsedInputTypes : ['voice'];
724
+ const containerStyle = this.getContainerStyle();
725
+ const buttons = (((_a = this.parsedTheme) === null || _a === void 0 ? void 0 : _a.buttons) || {});
726
+ const getSize = (type) => { var _a, _b; return ((_a = buttons[type]) === null || _a === void 0 ? void 0 : _a.size) || ((_b = buttons.voice) === null || _b === void 0 ? void 0 : _b.size) || '50px'; };
727
+ return (h("div", null, h("div", { class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h("div", { class: "row-audio-area" }, types.map(type => {
728
+ const size = getSize(type);
729
+ return h("div", { class: "skeleton-circle", style: { width: size, height: size } });
730
+ })), this.displayStatus && h("div", { class: "skeleton-status" }))));
731
+ }
595
732
  renderStatusMessage() {
733
+ const hasAnyError = this.hasError || this.hasTransientError;
596
734
  const statusClass = [
597
735
  'status-text',
598
- this.hasError && 'error',
599
- this.filledData && !this.hasError && 'success'
736
+ hasAnyError && 'error',
737
+ this.filledData && !hasAnyError && 'success'
600
738
  ].filter(Boolean).join(' ');
601
- return h("div", { class: statusClass }, this.statusMessage);
739
+ const statusStyle = this.getStatusStyle();
740
+ return h("div", { class: statusClass, style: statusStyle }, this.statusMessage);
602
741
  }
603
742
  renderFormPreview() {
604
743
  if (!this.parsedSchema)
@@ -770,9 +909,11 @@ export class VoiceFormRecorder {
770
909
  };
771
910
  }
772
911
  render() {
912
+ if (this.isInitializing) {
913
+ return this.renderLoadingSkeleton();
914
+ }
773
915
  const containerStyle = this.getContainerStyle();
774
- const statusStyle = this.getStatusStyle();
775
- return (h("div", { key: '8378e98f02e5b7482929d255c5ec12ff8c2731e4' }, h("div", { key: 'b8a0d873bd4e1b4c8936747c0919ac8c4c15301b', class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h("div", { key: '86e7783e3686db378ee16aa91a640f33c3255923', class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? h("div", { class: "status-text", style: statusStyle }, this.statusMessage) : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
916
+ return (h("div", null, h("div", { class: "voice-recorder-container" + (this.debug || this.renderForm ? "-debug" : ""), style: containerStyle }, h("div", { class: "row-audio-area" }, this.renderRecordButton(), this.renderUploadRecordButton(), this.renderUploadButton()), this.displayStatus ? this.renderStatusMessage() : "", this.renderForm ? this.renderFormPreview() : "", this.debug ? this.renderDebugPanel() : "")));
776
917
  }
777
918
  static get is() { return "voice-input-module"; }
778
919
  static get encapsulation() { return "shadow"; }
@@ -1127,10 +1268,12 @@ export class VoiceFormRecorder {
1127
1268
  "isProcessing": {},
1128
1269
  "statusMessage": {},
1129
1270
  "hasError": {},
1271
+ "hasTransientError": {},
1130
1272
  "transcription": {},
1131
1273
  "filledData": {},
1132
1274
  "debugInfo": {},
1133
- "isReadonlyMode": {}
1275
+ "isReadonlyMode": {},
1276
+ "isInitializing": {}
1134
1277
  };
1135
1278
  }
1136
1279
  static get events() {