Luigi commited on
Commit
9069ca4
·
1 Parent(s): 77e98bd

Add editable speaker names feature

Browse files

- Make speaker tags clickable for inline editing
- Add input field styling for speaker name editing
- Preserve user-edited names over LLM detections
- Support titles, job roles, and custom speaker labels
- Enter/Escape key support for editing
- Click-outside-to-save functionality
- Update state management for user-edited speaker names

Files changed (2) hide show
  1. frontend/app.js +77 -7
  2. frontend/styles.css +27 -0
frontend/app.js CHANGED
@@ -338,13 +338,13 @@ function renderTranscript() {
338
  const speakerTag = node.querySelector('.speaker-tag');
339
  if (typeof utt.speaker === 'number') {
340
  const speakerId = utt.speaker;
341
- const speakerName = state.speakerNames[speakerId]?.name;
342
- if (speakerName) {
343
- speakerTag.textContent = speakerName;
344
- } else {
345
- speakerTag.textContent = `Speaker ${speakerId + 1}`;
346
- }
347
  speakerTag.classList.remove('hidden');
 
 
 
348
  }
349
 
350
  fragment.appendChild(node);
@@ -434,9 +434,15 @@ function initAudioInteractions() {
434
  const editButton = event.target.closest('.edit-btn');
435
  const saveButton = event.target.closest('.save-edit');
436
  const cancelButton = event.target.closest('.cancel-edit');
 
437
 
438
  const index = Number(item.dataset.index);
439
 
 
 
 
 
 
440
  if (editButton) {
441
  toggleEdit(item, true);
442
  return;
@@ -478,6 +484,58 @@ function toggleEdit(item, editing) {
478
  }
479
  }
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  function seekToTime(timeInSeconds) {
482
  if (!Number.isFinite(timeInSeconds)) return;
483
  const audio = elements.audioPlayer;
@@ -576,7 +634,19 @@ async function handleSpeakerNameDetection() {
576
  if (!response.ok) throw new Error('Failed to detect speaker names');
577
 
578
  const speakerNames = await response.json();
579
- state.speakerNames = speakerNames;
 
 
 
 
 
 
 
 
 
 
 
 
580
 
581
  // Re-render transcript to show detected names
582
  renderTranscript();
 
338
  const speakerTag = node.querySelector('.speaker-tag');
339
  if (typeof utt.speaker === 'number') {
340
  const speakerId = utt.speaker;
341
+ const speakerInfo = state.speakerNames?.[speakerId];
342
+ const speakerName = speakerInfo?.name || `Speaker ${speakerId + 1}`;
343
+ speakerTag.textContent = speakerName;
 
 
 
344
  speakerTag.classList.remove('hidden');
345
+ speakerTag.classList.add('editable-speaker');
346
+ speakerTag.dataset.speakerId = speakerId;
347
+ speakerTag.title = 'Click to edit speaker name';
348
  }
349
 
350
  fragment.appendChild(node);
 
434
  const editButton = event.target.closest('.edit-btn');
435
  const saveButton = event.target.closest('.save-edit');
436
  const cancelButton = event.target.closest('.cancel-edit');
437
+ const speakerTag = event.target.closest('.editable-speaker');
438
 
439
  const index = Number(item.dataset.index);
440
 
441
+ if (speakerTag && !speakerTag.querySelector('input')) {
442
+ startSpeakerEdit(speakerTag);
443
+ return;
444
+ }
445
+
446
  if (editButton) {
447
  toggleEdit(item, true);
448
  return;
 
484
  }
485
  }
486
 
487
+ function startSpeakerEdit(speakerTag) {
488
+ const speakerId = Number(speakerTag.dataset.speakerId);
489
+ const currentName = speakerTag.textContent;
490
+
491
+ // Create input field
492
+ const input = document.createElement('input');
493
+ input.type = 'text';
494
+ input.className = 'speaker-edit-input';
495
+ input.value = currentName;
496
+ input.dataset.speakerId = speakerId;
497
+
498
+ // Replace speaker tag content with input
499
+ speakerTag.innerHTML = '';
500
+ speakerTag.appendChild(input);
501
+ input.focus();
502
+ input.select();
503
+
504
+ // Handle input events
505
+ const finishEdit = (save = true) => {
506
+ const newName = input.value.trim();
507
+ if (save && newName) {
508
+ // Update state
509
+ if (!state.speakerNames) state.speakerNames = {};
510
+ state.speakerNames[speakerId] = {
511
+ name: newName,
512
+ confidence: 'user', // Mark as user-edited
513
+ reason: 'User edited'
514
+ };
515
+ speakerTag.textContent = newName;
516
+ } else {
517
+ // Restore original name
518
+ const originalName = state.speakerNames?.[speakerId]?.name || `Speaker ${speakerId + 1}`;
519
+ speakerTag.textContent = originalName;
520
+ }
521
+ speakerTag.classList.add('editable-speaker');
522
+ };
523
+
524
+ input.addEventListener('keydown', (e) => {
525
+ if (e.key === 'Enter') {
526
+ e.preventDefault();
527
+ finishEdit(true);
528
+ } else if (e.key === 'Escape') {
529
+ e.preventDefault();
530
+ finishEdit(false);
531
+ }
532
+ });
533
+
534
+ input.addEventListener('blur', () => {
535
+ finishEdit(true);
536
+ });
537
+ }
538
+
539
  function seekToTime(timeInSeconds) {
540
  if (!Number.isFinite(timeInSeconds)) return;
541
  const audio = elements.audioPlayer;
 
634
  if (!response.ok) throw new Error('Failed to detect speaker names');
635
 
636
  const speakerNames = await response.json();
637
+
638
+ // Merge detected names with existing user-edited names (preserve user edits)
639
+ const mergedNames = { ...speakerNames };
640
+ if (state.speakerNames) {
641
+ Object.entries(state.speakerNames).forEach(([speakerId, info]) => {
642
+ if (info.confidence === 'user') {
643
+ // Preserve user-edited names
644
+ mergedNames[speakerId] = info;
645
+ }
646
+ });
647
+ }
648
+
649
+ state.speakerNames = mergedNames;
650
 
651
  // Re-render transcript to show detected names
652
  renderTranscript();
frontend/styles.css CHANGED
@@ -367,6 +367,33 @@ button:hover {
367
  background: rgba(129, 140, 248, 0.2);
368
  }
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  .utterance-actions {
371
  margin-left: auto;
372
  display: flex;
 
367
  background: rgba(129, 140, 248, 0.2);
368
  }
369
 
370
+ .editable-speaker {
371
+ cursor: pointer;
372
+ transition: all 0.2s ease;
373
+ border: 1px solid transparent;
374
+ }
375
+
376
+ .editable-speaker:hover {
377
+ background: rgba(129, 140, 248, 0.3);
378
+ border-color: rgba(129, 140, 248, 0.5);
379
+ }
380
+
381
+ .speaker-edit-input {
382
+ font-size: 0.75rem;
383
+ padding: 0.1rem 0.5rem;
384
+ border-radius: 999px;
385
+ background: rgba(30, 41, 59, 0.9);
386
+ border: 1px solid rgba(129, 140, 248, 0.8);
387
+ color: #e5e7eb;
388
+ width: 120px;
389
+ text-align: center;
390
+ }
391
+
392
+ .speaker-edit-input:focus {
393
+ outline: none;
394
+ border-color: rgba(129, 140, 248, 1);
395
+ }
396
+
397
  .utterance-actions {
398
  margin-left: auto;
399
  display: flex;