@autocode-cli/autocode 0.1.31 → 0.1.32

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 (47) hide show
  1. package/README.md +30 -0
  2. package/dist/cli/commands/health.d.ts +9 -0
  3. package/dist/cli/commands/health.d.ts.map +1 -0
  4. package/dist/cli/commands/health.js +101 -0
  5. package/dist/cli/commands/health.js.map +1 -0
  6. package/dist/cli/commands/move.d.ts.map +1 -1
  7. package/dist/cli/commands/move.js +14 -7
  8. package/dist/cli/commands/move.js.map +1 -1
  9. package/dist/cli/parser.d.ts.map +1 -1
  10. package/dist/cli/parser.js +2 -0
  11. package/dist/cli/parser.js.map +1 -1
  12. package/dist/core/hierarchy.d.ts +24 -0
  13. package/dist/core/hierarchy.d.ts.map +1 -1
  14. package/dist/core/hierarchy.js +76 -0
  15. package/dist/core/hierarchy.js.map +1 -1
  16. package/dist/core/issue.d.ts.map +1 -1
  17. package/dist/core/issue.js +11 -3
  18. package/dist/core/issue.js.map +1 -1
  19. package/dist/core/loop-detector.d.ts +45 -0
  20. package/dist/core/loop-detector.d.ts.map +1 -0
  21. package/dist/core/loop-detector.js +81 -0
  22. package/dist/core/loop-detector.js.map +1 -0
  23. package/dist/core/validation.d.ts +26 -0
  24. package/dist/core/validation.d.ts.map +1 -0
  25. package/dist/core/validation.js +67 -0
  26. package/dist/core/validation.js.map +1 -0
  27. package/dist/core/workflow.d.ts.map +1 -1
  28. package/dist/core/workflow.js +34 -1
  29. package/dist/core/workflow.js.map +1 -1
  30. package/dist/server/api.d.ts.map +1 -1
  31. package/dist/server/api.js +21 -0
  32. package/dist/server/api.js.map +1 -1
  33. package/dist/server/dashboard/pages/new-issue.d.ts.map +1 -1
  34. package/dist/server/dashboard/pages/new-issue.js +283 -3
  35. package/dist/server/dashboard/pages/new-issue.js.map +1 -1
  36. package/dist/server/dashboard/pages/new-issue.test.js +270 -0
  37. package/dist/server/dashboard/pages/new-issue.test.js.map +1 -1
  38. package/dist/services/monitoring.d.ts +47 -0
  39. package/dist/services/monitoring.d.ts.map +1 -0
  40. package/dist/services/monitoring.js +136 -0
  41. package/dist/services/monitoring.js.map +1 -0
  42. package/dist/types/index.d.ts +1 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/package.json +2 -1
  46. package/templates/prompts/in-progress.en.md +25 -9
  47. package/templates/prompts/in-progress.fr.md +25 -9
@@ -248,12 +248,114 @@ export function generateNewIssuePage(lang) {
248
248
  animation: spin 0.8s linear infinite;
249
249
  }
250
250
  @keyframes spin { to { transform: rotate(360deg); } }
251
+ @keyframes pulse-recording {
252
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7); }
253
+ 50% { box-shadow: 0 0 0 8px rgba(239, 68, 68, 0); }
254
+ }
255
+ .btn-mic {
256
+ background: var(--bg);
257
+ border: 1px solid var(--border);
258
+ color: var(--muted);
259
+ width: 42px;
260
+ height: 42px;
261
+ border-radius: 8px;
262
+ cursor: pointer;
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ transition: all 0.2s;
267
+ flex-shrink: 0;
268
+ }
269
+ .btn-mic:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
270
+ .btn-mic:disabled { opacity: 0.4; cursor: not-allowed; }
271
+ .btn-mic.recording {
272
+ background: var(--red);
273
+ border-color: var(--red);
274
+ color: white;
275
+ animation: pulse-recording 1.5s ease-in-out infinite;
276
+ }
277
+ .btn-mic svg { width: 18px; height: 18px; }
278
+ .description-row {
279
+ display: flex;
280
+ gap: 8px;
281
+ align-items: flex-start;
282
+ }
283
+ .description-row .form-group { flex: 1; margin-bottom: 0; }
251
284
  .title-row {
252
285
  display: flex;
253
286
  gap: 12px;
254
287
  align-items: flex-end;
255
288
  }
256
289
  .title-row .form-group { flex: 1; margin-bottom: 0; }
290
+ .title-input-row {
291
+ display: flex;
292
+ gap: 8px;
293
+ align-items: center;
294
+ }
295
+ .title-input-row input { flex: 1; }
296
+ .autocomplete-countdown {
297
+ position: fixed;
298
+ bottom: 80px;
299
+ right: 24px;
300
+ background: var(--bg-card);
301
+ border: 1px solid var(--accent);
302
+ border-radius: 12px;
303
+ padding: 16px 20px;
304
+ z-index: 100;
305
+ display: none;
306
+ flex-direction: column;
307
+ gap: 12px;
308
+ min-width: 280px;
309
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
310
+ }
311
+ .autocomplete-countdown.show { display: flex; }
312
+ .countdown-header {
313
+ display: flex;
314
+ align-items: center;
315
+ justify-content: space-between;
316
+ gap: 12px;
317
+ }
318
+ .countdown-text {
319
+ font-size: 14px;
320
+ color: var(--fg);
321
+ }
322
+ .countdown-timer {
323
+ font-weight: 600;
324
+ color: var(--accent);
325
+ font-size: 18px;
326
+ }
327
+ .countdown-progress {
328
+ height: 4px;
329
+ background: var(--border);
330
+ border-radius: 2px;
331
+ overflow: hidden;
332
+ }
333
+ .countdown-progress-bar {
334
+ height: 100%;
335
+ background: var(--accent);
336
+ transition: width 0.1s linear;
337
+ }
338
+ .countdown-actions {
339
+ display: flex;
340
+ gap: 8px;
341
+ }
342
+ .btn-countdown {
343
+ flex: 1;
344
+ padding: 8px 12px;
345
+ border-radius: 6px;
346
+ font-size: 13px;
347
+ cursor: pointer;
348
+ border: 1px solid var(--border);
349
+ background: var(--bg);
350
+ color: var(--fg);
351
+ }
352
+ .btn-countdown:hover { border-color: var(--accent); }
353
+ .btn-countdown.primary {
354
+ background: var(--accent);
355
+ border-color: var(--accent);
356
+ color: white;
357
+ }
358
+ .btn-countdown.primary:hover { opacity: 0.9; }
257
359
  .btn-secondary {
258
360
  background: var(--bg);
259
361
  color: var(--fg);
@@ -294,7 +396,16 @@ export function generateNewIssuePage(lang) {
294
396
  <div class="title-row">
295
397
  <div class="form-group">
296
398
  <label for="issue-title" data-i18n="newIssue.titleLabel">Title *</label>
297
- <input type="text" id="issue-title" placeholder="E.g.: Fix the login bug" data-i18n-placeholder="newIssue.titlePlaceholder">
399
+ <div class="title-input-row">
400
+ <input type="text" id="issue-title" placeholder="E.g.: Fix the login bug" data-i18n-placeholder="newIssue.titlePlaceholder">
401
+ <button type="button" class="btn-mic" id="btn-mic" onclick="toggleSpeechToText()" title="Speech to text">
402
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
403
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/>
404
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
405
+ <line x1="12" x2="12" y1="19" y2="22"/>
406
+ </svg>
407
+ </button>
408
+ </div>
298
409
  </div>
299
410
  <button type="button" class="btn-autocomplete" id="btn-autocomplete" onclick="autocomplete()" title="Auto-fill fields using AI">
300
411
  <span id="autocomplete-icon">&#10024;</span>
@@ -371,6 +482,20 @@ export function generateNewIssuePage(lang) {
371
482
 
372
483
  <div class="notification" id="notification"></div>
373
484
 
485
+ <div class="autocomplete-countdown" id="autocomplete-countdown">
486
+ <div class="countdown-header">
487
+ <span class="countdown-text" data-i18n="countdown.autocompletingIn">AutoComplete in</span>
488
+ <span class="countdown-timer" id="countdown-timer">3</span>
489
+ </div>
490
+ <div class="countdown-progress">
491
+ <div class="countdown-progress-bar" id="countdown-progress-bar" style="width: 100%"></div>
492
+ </div>
493
+ <div class="countdown-actions">
494
+ <button class="btn-countdown" onclick="cancelAutocompleteCountdown()" data-i18n="countdown.cancel">Cancel</button>
495
+ <button class="btn-countdown primary" onclick="triggerAutocompleteNow()" data-i18n="countdown.now">Now</button>
496
+ </div>
497
+ </div>
498
+
374
499
  <script>
375
500
  const STORAGE_KEY = 'autocode-lang';
376
501
  let currentLang = localStorage.getItem(STORAGE_KEY) || 'fr';
@@ -407,7 +532,16 @@ export function generateNewIssuePage(lang) {
407
532
  'notify.error': 'Error',
408
533
  'notify.titleRequired': 'Title is required',
409
534
  'notify.autocompleteSuccess': 'Fields auto-filled!',
410
- 'notify.autocompleteTitleRequired': 'Enter a title first'
535
+ 'notify.autocompleteTitleRequired': 'Enter a title first',
536
+ 'mic.title': 'Speech to text',
537
+ 'mic.titleRecording': 'Recording... Click to stop',
538
+ 'mic.notSupported': 'Speech recognition not supported',
539
+ 'notify.micNotSupported': 'Speech recognition is not supported by your browser',
540
+ 'notify.micError': 'Speech recognition error',
541
+ 'countdown.autocompletingIn': 'AutoComplete in',
542
+ 'countdown.cancel': 'Cancel',
543
+ 'countdown.now': 'Now',
544
+ 'countdown.seconds': 's'
411
545
  },
412
546
  fr: {
413
547
  'newIssue.title': 'Nouveau ticket',
@@ -439,7 +573,16 @@ export function generateNewIssuePage(lang) {
439
573
  'notify.error': 'Erreur',
440
574
  'notify.titleRequired': 'Le titre est requis',
441
575
  'notify.autocompleteSuccess': 'Champs remplis automatiquement !',
442
- 'notify.autocompleteTitleRequired': 'Entrez un titre d\\'abord'
576
+ 'notify.autocompleteTitleRequired': 'Entrez un titre d\\'abord',
577
+ 'mic.title': 'Dictée vocale',
578
+ 'mic.titleRecording': 'Enregistrement... Cliquer pour arrêter',
579
+ 'mic.notSupported': 'Reconnaissance vocale non supportée',
580
+ 'notify.micNotSupported': 'La reconnaissance vocale n\\'est pas supportée par votre navigateur',
581
+ 'notify.micError': 'Erreur de reconnaissance vocale',
582
+ 'countdown.autocompletingIn': 'AutoComplete dans',
583
+ 'countdown.cancel': 'Annuler',
584
+ 'countdown.now': 'Maintenant',
585
+ 'countdown.seconds': 's'
443
586
  }
444
587
  };
445
588
 
@@ -561,6 +704,7 @@ export function generateNewIssuePage(lang) {
561
704
  currentLang = newLang;
562
705
  localStorage.setItem(STORAGE_KEY, newLang);
563
706
  updateLangUI();
707
+ updateMicButton();
564
708
  }
565
709
  });
566
710
  });
@@ -639,8 +783,144 @@ export function generateNewIssuePage(lang) {
639
783
  }
640
784
  }
641
785
 
786
+ // Speech-to-text
787
+ let recognition = null;
788
+ let isRecording = false;
789
+ let autocompleteCountdownTimer = null;
790
+ let autocompleteCountdownInterval = null;
791
+ const AUTOCOMPLETE_DELAY_SECONDS = 3;
792
+
793
+ function isSpeechSupported() {
794
+ return 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
795
+ }
796
+
797
+ function initSpeechRecognition() {
798
+ if (!isSpeechSupported()) return null;
799
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
800
+ const rec = new SpeechRecognition();
801
+ rec.continuous = true;
802
+ rec.interimResults = true;
803
+ rec.lang = currentLang === 'fr' ? 'fr-FR' : 'en-US';
804
+ return rec;
805
+ }
806
+
807
+ function updateMicButton() {
808
+ const btn = document.getElementById('btn-mic');
809
+ if (!isSpeechSupported()) {
810
+ btn.disabled = true;
811
+ btn.title = t('mic.notSupported');
812
+ } else {
813
+ btn.disabled = false;
814
+ btn.title = isRecording ? t('mic.titleRecording') : t('mic.title');
815
+ btn.classList.toggle('recording', isRecording);
816
+ }
817
+ }
818
+
819
+ function handleSpeechResult(event, titleInput, state) {
820
+ for (let i = event.resultIndex; i < event.results.length; i++) {
821
+ const result = event.results[i];
822
+ if (result.isFinal) {
823
+ const text = result[0].transcript;
824
+ state.finalTranscript += (state.finalTranscript ? ' ' : '') + text;
825
+ titleInput.value = state.finalTranscript;
826
+ }
827
+ }
828
+ }
829
+
830
+ function handleSpeechError(event) {
831
+ if (event.error !== 'aborted') {
832
+ showNotification(t('notify.micError') + ': ' + event.error, true);
833
+ }
834
+ isRecording = false;
835
+ updateMicButton();
836
+ }
837
+
838
+ function handleSpeechEnd() {
839
+ isRecording = false;
840
+ updateMicButton();
841
+ const title = document.getElementById('issue-title').value.trim();
842
+ if (title) {
843
+ startAutocompleteCountdown();
844
+ }
845
+ }
846
+
847
+ function startAutocompleteCountdown() {
848
+ const countdown = document.getElementById('autocomplete-countdown');
849
+ const timerEl = document.getElementById('countdown-timer');
850
+ const progressBar = document.getElementById('countdown-progress-bar');
851
+
852
+ let remaining = AUTOCOMPLETE_DELAY_SECONDS;
853
+ timerEl.textContent = remaining + t('countdown.seconds');
854
+ progressBar.style.width = '100%';
855
+ countdown.classList.add('show');
856
+
857
+ autocompleteCountdownInterval = setInterval(function() {
858
+ remaining -= 0.1;
859
+ const percentage = (remaining / AUTOCOMPLETE_DELAY_SECONDS) * 100;
860
+ progressBar.style.width = percentage + '%';
861
+ if (remaining <= 0) {
862
+ remaining = 0;
863
+ }
864
+ timerEl.textContent = Math.ceil(remaining) + t('countdown.seconds');
865
+ }, 100);
866
+
867
+ autocompleteCountdownTimer = setTimeout(function() {
868
+ hideAutocompleteCountdown();
869
+ autocomplete();
870
+ }, AUTOCOMPLETE_DELAY_SECONDS * 1000);
871
+ }
872
+
873
+ function hideAutocompleteCountdown() {
874
+ const countdown = document.getElementById('autocomplete-countdown');
875
+ countdown.classList.remove('show');
876
+ if (autocompleteCountdownTimer) {
877
+ clearTimeout(autocompleteCountdownTimer);
878
+ autocompleteCountdownTimer = null;
879
+ }
880
+ if (autocompleteCountdownInterval) {
881
+ clearInterval(autocompleteCountdownInterval);
882
+ autocompleteCountdownInterval = null;
883
+ }
884
+ }
885
+
886
+ function cancelAutocompleteCountdown() {
887
+ hideAutocompleteCountdown();
888
+ }
889
+
890
+ function triggerAutocompleteNow() {
891
+ hideAutocompleteCountdown();
892
+ autocomplete();
893
+ }
894
+
895
+ function toggleSpeechToText() {
896
+ if (!isSpeechSupported()) {
897
+ showNotification(t('notify.micNotSupported'), true);
898
+ return;
899
+ }
900
+
901
+ if (isRecording && recognition) {
902
+ recognition.stop();
903
+ return;
904
+ }
905
+
906
+ recognition = initSpeechRecognition();
907
+ if (!recognition) return;
908
+
909
+ const titleInput = document.getElementById('issue-title');
910
+ const state = { finalTranscript: titleInput.value };
911
+
912
+ recognition.onresult = function(event) { handleSpeechResult(event, titleInput, state); };
913
+ recognition.onerror = handleSpeechError;
914
+ recognition.onend = handleSpeechEnd;
915
+
916
+ recognition.start();
917
+ isRecording = true;
918
+ updateMicButton();
919
+ }
920
+
642
921
  // Init
643
922
  updateLangUI();
923
+ updateMicButton();
644
924
  </script>
645
925
  </body>
646
926
  </html>`;
@@ -1 +1 @@
1
- {"version":3,"file":"new-issue.js","sourceRoot":"","sources":["../../../../src/server/dashboard/pages/new-issue.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAErE,OAAO;cACK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmUJ,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,kBAAkB,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAuTjJ,CAAC;AACT,CAAC"}
1
+ {"version":3,"file":"new-issue.js","sourceRoot":"","sources":["../../../../src/server/dashboard/pages/new-issue.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IAErE,OAAO;cACK,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkbJ,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,kBAAkB,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,IAAI,WAAW,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAgejJ,CAAC;AACT,CAAC"}
@@ -327,6 +327,276 @@ describe('AutoComplete functionality', () => {
327
327
  });
328
328
  });
329
329
  });
330
+ describe('Speech-to-Text functionality', () => {
331
+ describe('UI elements', () => {
332
+ it('should include microphone button', () => {
333
+ const html = generateNewIssuePage('en');
334
+ expect(html).toContain('id="btn-mic"');
335
+ expect(html).toContain('class="btn-mic"');
336
+ expect(html).toContain('onclick="toggleSpeechToText()"');
337
+ });
338
+ it('should include microphone SVG icon', () => {
339
+ const html = generateNewIssuePage('en');
340
+ expect(html).toContain('<svg');
341
+ expect(html).toContain('viewBox="0 0 24 24"');
342
+ // Microphone path
343
+ expect(html).toContain('M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5');
344
+ });
345
+ it('should include microphone button styling', () => {
346
+ const html = generateNewIssuePage('en');
347
+ expect(html).toContain('.btn-mic');
348
+ expect(html).toContain('.btn-mic:hover');
349
+ expect(html).toContain('.btn-mic:disabled');
350
+ expect(html).toContain('.btn-mic.recording');
351
+ });
352
+ it('should include recording animation', () => {
353
+ const html = generateNewIssuePage('en');
354
+ expect(html).toContain('@keyframes pulse-recording');
355
+ expect(html).toContain('animation: pulse-recording');
356
+ });
357
+ it('should place microphone button next to title input', () => {
358
+ const html = generateNewIssuePage('en');
359
+ expect(html).toContain('class="title-input-row"');
360
+ // Title and button should be in same container
361
+ expect(html).toMatch(/title-input-row[\s\S]*?issue-title[\s\S]*?btn-mic/);
362
+ });
363
+ });
364
+ describe('JavaScript functionality', () => {
365
+ it('should include speech recognition state variables', () => {
366
+ const html = generateNewIssuePage('en');
367
+ expect(html).toContain('let recognition = null');
368
+ expect(html).toContain('let isRecording = false');
369
+ });
370
+ it('should include isSpeechSupported function', () => {
371
+ const html = generateNewIssuePage('en');
372
+ expect(html).toContain('function isSpeechSupported()');
373
+ expect(html).toContain("'SpeechRecognition' in window");
374
+ expect(html).toContain("'webkitSpeechRecognition' in window");
375
+ });
376
+ it('should include initSpeechRecognition function', () => {
377
+ const html = generateNewIssuePage('en');
378
+ expect(html).toContain('function initSpeechRecognition()');
379
+ expect(html).toContain('window.SpeechRecognition || window.webkitSpeechRecognition');
380
+ expect(html).toContain('new SpeechRecognition()');
381
+ });
382
+ it('should configure continuous and interimResults mode', () => {
383
+ const html = generateNewIssuePage('en');
384
+ expect(html).toContain('rec.continuous = true');
385
+ expect(html).toContain('rec.interimResults = true');
386
+ });
387
+ it('should include updateMicButton function', () => {
388
+ const html = generateNewIssuePage('en');
389
+ expect(html).toContain('function updateMicButton()');
390
+ expect(html).toContain("btn.classList.toggle('recording', isRecording)");
391
+ });
392
+ it('should include toggleSpeechToText function', () => {
393
+ const html = generateNewIssuePage('en');
394
+ expect(html).toContain('function toggleSpeechToText()');
395
+ });
396
+ it('should handle push-to-talk toggle - start recording', () => {
397
+ const html = generateNewIssuePage('en');
398
+ expect(html).toContain('recognition.start()');
399
+ expect(html).toContain('isRecording = true');
400
+ });
401
+ it('should handle push-to-talk toggle - stop recording', () => {
402
+ const html = generateNewIssuePage('en');
403
+ expect(html).toContain('if (isRecording && recognition)');
404
+ expect(html).toContain('recognition.stop()');
405
+ });
406
+ it('should include onresult event handler', () => {
407
+ const html = generateNewIssuePage('en');
408
+ expect(html).toContain('recognition.onresult = function(event)');
409
+ expect(html).toContain('event.resultIndex');
410
+ expect(html).toContain('result.isFinal');
411
+ expect(html).toContain('result[0].transcript');
412
+ });
413
+ it('should append transcribed text to title input', () => {
414
+ const html = generateNewIssuePage('en');
415
+ expect(html).toContain("const titleInput = document.getElementById('issue-title')");
416
+ // The handleSpeechResult function assigns to titleInput.value
417
+ expect(html).toContain('titleInput.value = state.finalTranscript');
418
+ });
419
+ it('should include onerror event handler', () => {
420
+ const html = generateNewIssuePage('en');
421
+ // Uses a separate handler function
422
+ expect(html).toContain('recognition.onerror = handleSpeechError');
423
+ expect(html).toContain("event.error !== 'aborted'");
424
+ });
425
+ it('should include onend event handler', () => {
426
+ const html = generateNewIssuePage('en');
427
+ // Uses a separate handler function
428
+ expect(html).toContain('recognition.onend = handleSpeechEnd');
429
+ expect(html).toContain('function handleSpeechEnd()');
430
+ });
431
+ it('should initialize mic button on page load', () => {
432
+ const html = generateNewIssuePage('en');
433
+ expect(html).toContain('updateMicButton()');
434
+ });
435
+ });
436
+ describe('Language support (FR and EN)', () => {
437
+ it('should set language to fr-FR for French', () => {
438
+ const html = generateNewIssuePage('fr');
439
+ expect(html).toContain("rec.lang = currentLang === 'fr' ? 'fr-FR' : 'en-US'");
440
+ });
441
+ it('should set language to en-US for English', () => {
442
+ const html = generateNewIssuePage('en');
443
+ expect(html).toContain("rec.lang = currentLang === 'fr' ? 'fr-FR' : 'en-US'");
444
+ });
445
+ });
446
+ describe('Error handling', () => {
447
+ it('should show notification when speech not supported', () => {
448
+ const html = generateNewIssuePage('en');
449
+ expect(html).toContain('if (!isSpeechSupported())');
450
+ expect(html).toContain("showNotification(t('notify.micNotSupported'), true)");
451
+ });
452
+ it('should disable button when speech not supported', () => {
453
+ const html = generateNewIssuePage('en');
454
+ expect(html).toContain('btn.disabled = true');
455
+ expect(html).toContain("btn.title = t('mic.notSupported')");
456
+ });
457
+ it('should show error notification on recognition error', () => {
458
+ const html = generateNewIssuePage('en');
459
+ expect(html).toContain("showNotification(t('notify.micError') + ': ' + event.error, true)");
460
+ });
461
+ it('should ignore aborted error', () => {
462
+ const html = generateNewIssuePage('en');
463
+ expect(html).toContain("if (event.error !== 'aborted')");
464
+ });
465
+ it('should reset recording state on error', () => {
466
+ const html = generateNewIssuePage('en');
467
+ // handleSpeechError function should reset isRecording and update button
468
+ expect(html).toContain('function handleSpeechError(event)');
469
+ expect(html).toMatch(/handleSpeechError[\s\S]*?isRecording = false[\s\S]*?updateMicButton\(\)/);
470
+ });
471
+ it('should handle null recognition gracefully', () => {
472
+ const html = generateNewIssuePage('en');
473
+ expect(html).toContain('if (!recognition) return');
474
+ });
475
+ });
476
+ describe('i18n translations for Speech-to-Text', () => {
477
+ it('should include English Speech-to-Text translations', () => {
478
+ const html = generateNewIssuePage('en');
479
+ expect(html).toContain("'mic.title': 'Speech to text'");
480
+ expect(html).toContain("'mic.titleRecording': 'Recording... Click to stop'");
481
+ expect(html).toContain("'mic.notSupported': 'Speech recognition not supported'");
482
+ expect(html).toContain("'notify.micNotSupported': 'Speech recognition is not supported by your browser'");
483
+ expect(html).toContain("'notify.micError': 'Speech recognition error'");
484
+ });
485
+ it('should include French Speech-to-Text translations', () => {
486
+ const html = generateNewIssuePage('fr');
487
+ expect(html).toContain("'mic.title': 'Dictée vocale'");
488
+ expect(html).toContain("'mic.titleRecording': 'Enregistrement... Cliquer pour arrêter'");
489
+ expect(html).toContain("'mic.notSupported': 'Reconnaissance vocale non supportée'");
490
+ expect(html).toContain("'notify.micNotSupported': 'La reconnaissance vocale n\\'est pas supportée par votre navigateur'");
491
+ expect(html).toContain("'notify.micError': 'Erreur de reconnaissance vocale'");
492
+ });
493
+ it('should update mic button title based on recording state', () => {
494
+ const html = generateNewIssuePage('en');
495
+ expect(html).toContain("btn.title = isRecording ? t('mic.titleRecording') : t('mic.title')");
496
+ });
497
+ });
498
+ describe('Recording state management', () => {
499
+ it('should track recording state with isRecording variable', () => {
500
+ const html = generateNewIssuePage('en');
501
+ expect(html).toContain('let isRecording = false');
502
+ });
503
+ it('should toggle recording class on button', () => {
504
+ const html = generateNewIssuePage('en');
505
+ expect(html).toContain("btn.classList.toggle('recording', isRecording)");
506
+ });
507
+ it('should apply red background when recording', () => {
508
+ const html = generateNewIssuePage('en');
509
+ expect(html).toContain('.btn-mic.recording');
510
+ expect(html).toContain('background: var(--red)');
511
+ });
512
+ it('should update button state after recognition ends', () => {
513
+ const html = generateNewIssuePage('en');
514
+ // onend handler should update button
515
+ expect(html).toMatch(/onend[\s\S]*?updateMicButton\(\)/);
516
+ });
517
+ });
518
+ describe('Autocomplete countdown after transcription', () => {
519
+ it('should trigger autocomplete countdown after speech ends if title has content', () => {
520
+ const html = generateNewIssuePage('en');
521
+ expect(html).toContain('function handleSpeechEnd()');
522
+ expect(html).toContain("const title = document.getElementById('issue-title').value.trim()");
523
+ expect(html).toContain('if (title) {');
524
+ expect(html).toContain('startAutocompleteCountdown()');
525
+ });
526
+ it('should have configurable autocomplete delay', () => {
527
+ const html = generateNewIssuePage('en');
528
+ expect(html).toContain('const AUTOCOMPLETE_DELAY_SECONDS = 3');
529
+ });
530
+ it('should show countdown UI element', () => {
531
+ const html = generateNewIssuePage('en');
532
+ expect(html).toContain('id="autocomplete-countdown"');
533
+ expect(html).toContain('class="autocomplete-countdown"');
534
+ expect(html).toContain('id="countdown-timer"');
535
+ expect(html).toContain('id="countdown-progress-bar"');
536
+ });
537
+ it('should have cancel and now buttons in countdown', () => {
538
+ const html = generateNewIssuePage('en');
539
+ expect(html).toContain('onclick="cancelAutocompleteCountdown()"');
540
+ expect(html).toContain('onclick="triggerAutocompleteNow()"');
541
+ });
542
+ it('should define startAutocompleteCountdown function', () => {
543
+ const html = generateNewIssuePage('en');
544
+ expect(html).toContain('function startAutocompleteCountdown()');
545
+ expect(html).toContain("countdown.classList.add('show')");
546
+ });
547
+ it('should define cancelAutocompleteCountdown function', () => {
548
+ const html = generateNewIssuePage('en');
549
+ expect(html).toContain('function cancelAutocompleteCountdown()');
550
+ expect(html).toContain('hideAutocompleteCountdown()');
551
+ });
552
+ it('should define triggerAutocompleteNow function', () => {
553
+ const html = generateNewIssuePage('en');
554
+ expect(html).toContain('function triggerAutocompleteNow()');
555
+ expect(html).toContain('hideAutocompleteCountdown()');
556
+ expect(html).toContain('autocomplete()');
557
+ });
558
+ it('should have countdown translations in English', () => {
559
+ const html = generateNewIssuePage('en');
560
+ expect(html).toContain("'countdown.autocompletingIn': 'AutoComplete in'");
561
+ expect(html).toContain("'countdown.cancel': 'Cancel'");
562
+ expect(html).toContain("'countdown.now': 'Now'");
563
+ expect(html).toContain("'countdown.seconds': 's'");
564
+ });
565
+ it('should have countdown translations in French', () => {
566
+ const html = generateNewIssuePage('fr');
567
+ expect(html).toContain("'countdown.autocompletingIn': 'AutoComplete dans'");
568
+ expect(html).toContain("'countdown.cancel': 'Annuler'");
569
+ expect(html).toContain("'countdown.now': 'Maintenant'");
570
+ expect(html).toContain("'countdown.seconds': 's'");
571
+ });
572
+ it('should clear timers in hideAutocompleteCountdown', () => {
573
+ const html = generateNewIssuePage('en');
574
+ expect(html).toContain('function hideAutocompleteCountdown()');
575
+ expect(html).toContain('clearTimeout(autocompleteCountdownTimer)');
576
+ expect(html).toContain('clearInterval(autocompleteCountdownInterval)');
577
+ });
578
+ it('should have countdown progress bar with animation', () => {
579
+ const html = generateNewIssuePage('en');
580
+ expect(html).toContain('.countdown-progress-bar');
581
+ expect(html).toContain("progressBar.style.width = percentage + '%'");
582
+ });
583
+ });
584
+ describe('Microphone button placement', () => {
585
+ it('should have mic button next to title input', () => {
586
+ const html = generateNewIssuePage('en');
587
+ expect(html).toContain('class="title-input-row"');
588
+ expect(html).toContain('id="issue-title"');
589
+ expect(html).toContain('id="btn-mic"');
590
+ });
591
+ it('should not have mic button in description row', () => {
592
+ const html = generateNewIssuePage('en');
593
+ // Description should be a simple textarea without mic button
594
+ expect(html).toContain('id="issue-description"');
595
+ // The description-row class should not contain btn-mic anymore
596
+ expect(html).not.toContain('class="description-row">\n <textarea');
597
+ });
598
+ });
599
+ });
330
600
  describe('Edge cases', () => {
331
601
  it('should handle empty columns array', async () => {
332
602
  // Re-mock with empty columns