lyrics-transcriber 0.45.0__py3-none-any.whl → 0.47.0__py3-none-any.whl

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 (26) hide show
  1. lyrics_transcriber/frontend/.yarn/releases/{yarn-4.6.0.cjs → yarn-4.7.0.cjs} +292 -291
  2. lyrics_transcriber/frontend/.yarnrc.yml +1 -1
  3. lyrics_transcriber/frontend/dist/assets/{index-ZCT0s9MG.js → index-2vK-qVJS.js} +231 -151
  4. lyrics_transcriber/frontend/dist/assets/index-2vK-qVJS.js.map +1 -0
  5. lyrics_transcriber/frontend/dist/index.html +1 -1
  6. lyrics_transcriber/frontend/package.json +1 -1
  7. lyrics_transcriber/frontend/src/components/EditModal.tsx +36 -36
  8. lyrics_transcriber/frontend/src/components/EditWordList.tsx +73 -2
  9. lyrics_transcriber/frontend/src/components/Header.tsx +1 -16
  10. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +26 -8
  11. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +35 -16
  12. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +3 -1
  13. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +4 -2
  14. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +26 -3
  15. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +2 -1
  16. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +3 -2
  17. lyrics_transcriber/frontend/src/components/shared/types.ts +2 -0
  18. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +45 -8
  19. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +47 -0
  20. lyrics_transcriber/frontend/src/types.ts +1 -1
  21. {lyrics_transcriber-0.45.0.dist-info → lyrics_transcriber-0.47.0.dist-info}/METADATA +1 -1
  22. {lyrics_transcriber-0.45.0.dist-info → lyrics_transcriber-0.47.0.dist-info}/RECORD +25 -25
  23. lyrics_transcriber/frontend/dist/assets/index-ZCT0s9MG.js.map +0 -1
  24. {lyrics_transcriber-0.45.0.dist-info → lyrics_transcriber-0.47.0.dist-info}/LICENSE +0 -0
  25. {lyrics_transcriber-0.45.0.dist-info → lyrics_transcriber-0.47.0.dist-info}/WHEEL +0 -0
  26. {lyrics_transcriber-0.45.0.dist-info → lyrics_transcriber-0.47.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Lyrics Transcriber Analyzer</title>
8
- <script type="module" crossorigin src="/assets/index-ZCT0s9MG.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-2vK-qVJS.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -38,5 +38,5 @@
38
38
  "typescript-eslint": "^8.18.2",
39
39
  "vite": "^6.0.5"
40
40
  },
41
- "packageManager": "yarn@4.6.0"
41
+ "packageManager": "yarn@4.7.0"
42
42
  }
@@ -185,15 +185,15 @@ export default function EditModal({
185
185
  isGlobal = false,
186
186
  isLoading = false
187
187
  }: EditModalProps) {
188
- console.log('EditModal - Render', {
189
- open,
190
- isGlobal,
191
- isLoading,
192
- hasSegment: !!segment,
193
- segmentIndex,
194
- hasOriginalSegment: !!originalSegment,
195
- hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
- });
188
+ // console.log('EditModal - Render', {
189
+ // open,
190
+ // isGlobal,
191
+ // isLoading,
192
+ // hasSegment: !!segment,
193
+ // segmentIndex,
194
+ // hasOriginalSegment: !!originalSegment,
195
+ // hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
+ // });
197
197
 
198
198
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
199
199
  const [isPlaying, setIsPlaying] = useState(false)
@@ -233,7 +233,7 @@ export default function EditModal({
233
233
  })
234
234
 
235
235
  const handleClose = useCallback(() => {
236
- console.log('EditModal - handleClose called');
236
+ // console.log('EditModal - handleClose called');
237
237
  cleanupManualSync()
238
238
  onClose()
239
239
  }, [onClose, cleanupManualSync])
@@ -243,12 +243,12 @@ export default function EditModal({
243
243
  const spacebarHandler = handleSpacebar // Capture the current handler
244
244
 
245
245
  if (open) {
246
- console.log('EditModal - Setting up modal spacebar handler', {
247
- hasPlaySegment: !!onPlaySegment,
248
- editedSegmentId: editedSegment?.id,
249
- handlerFunction: spacebarHandler.toString().slice(0, 100),
250
- isLoading
251
- })
246
+ // console.log('EditModal - Setting up modal spacebar handler', {
247
+ // hasPlaySegment: !!onPlaySegment,
248
+ // editedSegmentId: editedSegment?.id,
249
+ // handlerFunction: spacebarHandler.toString().slice(0, 100),
250
+ // isLoading
251
+ // })
252
252
 
253
253
  // Create a function that will be called by the global event listeners
254
254
  const handleKeyEvent = (e: KeyboardEvent) => {
@@ -262,7 +262,7 @@ export default function EditModal({
262
262
  // Only cleanup when the effect is re-run or the modal is closed
263
263
  return () => {
264
264
  if (!open) {
265
- console.log('EditModal - Cleanup: clearing modal spacebar handler')
265
+ // console.log('EditModal - Cleanup: clearing modal spacebar handler')
266
266
  setModalSpacebarHandler(undefined)
267
267
  }
268
268
  }
@@ -289,11 +289,11 @@ export default function EditModal({
289
289
 
290
290
  // All useEffect hooks
291
291
  useEffect(() => {
292
- console.log('EditModal - segment changed', {
293
- hasSegment: !!segment,
294
- segmentId: segment?.id,
295
- wordCount: segment?.words.length
296
- });
292
+ // console.log('EditModal - segment changed', {
293
+ // hasSegment: !!segment,
294
+ // segmentId: segment?.id,
295
+ // wordCount: segment?.words.length
296
+ // });
297
297
  setEditedSegment(segment)
298
298
  }, [segment])
299
299
 
@@ -304,7 +304,7 @@ export default function EditModal({
304
304
  const endTime = editedSegment.end_time ?? 0
305
305
 
306
306
  if (window.isAudioPlaying && currentTime > endTime) {
307
- console.log('Stopping playback: current time exceeded end time')
307
+ // console.log('Stopping playback: current time exceeded end time')
308
308
  window.toggleAudioPlayback?.()
309
309
  cleanupManualSync()
310
310
  }
@@ -519,7 +519,7 @@ export default function EditModal({
519
519
 
520
520
  // Memoize the dialog title to prevent re-renders
521
521
  const dialogTitle = useMemo(() => {
522
- console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
522
+ // console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
523
523
 
524
524
  if (isLoading) {
525
525
  return (
@@ -563,24 +563,24 @@ export default function EditModal({
563
563
 
564
564
  // Early return after all hooks and function definitions
565
565
  if (!isLoading && (!segment || !editedSegment || !originalSegment)) {
566
- console.log('EditModal - Early return: missing required data', {
567
- hasSegment: !!segment,
568
- hasEditedSegment: !!editedSegment,
569
- hasOriginalSegment: !!originalSegment,
570
- isLoading
571
- });
566
+ // console.log('EditModal - Early return: missing required data', {
567
+ // hasSegment: !!segment,
568
+ // hasEditedSegment: !!editedSegment,
569
+ // hasOriginalSegment: !!originalSegment,
570
+ // isLoading
571
+ // });
572
572
  return null;
573
573
  }
574
574
  if (!isLoading && !isGlobal && segmentIndex === null) {
575
- console.log('EditModal - Early return: non-global mode with null segmentIndex');
575
+ // console.log('EditModal - Early return: non-global mode with null segmentIndex');
576
576
  return null;
577
577
  }
578
578
 
579
- console.log('EditModal - Rendering dialog content', {
580
- isLoading,
581
- hasEditedSegment: !!editedSegment,
582
- hasOriginalSegment: !!originalSegment
583
- });
579
+ // console.log('EditModal - Rendering dialog content', {
580
+ // isLoading,
581
+ // hasEditedSegment: !!editedSegment,
582
+ // hasOriginalSegment: !!originalSegment
583
+ // });
584
584
 
585
585
  return (
586
586
  <Dialog
@@ -33,7 +33,8 @@ const WordRow = memo(function WordRow({
33
33
  onWordUpdate,
34
34
  onSplitWord,
35
35
  onRemoveWord,
36
- wordsLength
36
+ wordsLength,
37
+ onTabNavigation
37
38
  }: {
38
39
  word: Word
39
40
  index: number
@@ -41,7 +42,17 @@ const WordRow = memo(function WordRow({
41
42
  onSplitWord: (index: number) => void
42
43
  onRemoveWord: (index: number) => void
43
44
  wordsLength: number
45
+ onTabNavigation: (currentIndex: number) => void
44
46
  }) {
47
+ const handleKeyDown = (e: React.KeyboardEvent) => {
48
+ // console.log('KeyDown event:', e.key, 'Shift:', e.shiftKey, 'Index:', index);
49
+ if (e.key === 'Tab' && !e.shiftKey) {
50
+ // console.log('Tab key detected, preventing default and navigating');
51
+ e.preventDefault();
52
+ onTabNavigation(index);
53
+ }
54
+ };
55
+
45
56
  return (
46
57
  <Box sx={{
47
58
  display: 'flex',
@@ -53,8 +64,10 @@ const WordRow = memo(function WordRow({
53
64
  label={`Word ${index}`}
54
65
  value={word.text}
55
66
  onChange={(e) => onWordUpdate(index, { text: e.target.value })}
67
+ onKeyDown={handleKeyDown}
56
68
  fullWidth
57
69
  size="small"
70
+ id={`word-text-${index}`}
58
71
  />
59
72
  <TextField
60
73
  label="Start Time"
@@ -108,7 +121,8 @@ const WordItem = memo(function WordItem({
108
121
  onAddSegment,
109
122
  onMergeSegment,
110
123
  wordsLength,
111
- isGlobal
124
+ isGlobal,
125
+ onTabNavigation
112
126
  }: {
113
127
  word: Word
114
128
  index: number
@@ -122,6 +136,7 @@ const WordItem = memo(function WordItem({
122
136
  onMergeSegment?: (mergeWithNext: boolean) => void
123
137
  wordsLength: number
124
138
  isGlobal: boolean
139
+ onTabNavigation: (currentIndex: number) => void
125
140
  }) {
126
141
  return (
127
142
  <Box key={word.id}>
@@ -132,6 +147,7 @@ const WordItem = memo(function WordItem({
132
147
  onSplitWord={onSplitWord}
133
148
  onRemoveWord={onRemoveWord}
134
149
  wordsLength={wordsLength}
150
+ onTabNavigation={onTabNavigation}
135
151
  />
136
152
 
137
153
  {/* Word divider with merge/split functionality */}
@@ -210,6 +226,60 @@ export default function EditWordList({
210
226
  setPage(value);
211
227
  };
212
228
 
229
+ // Handle tab navigation between word text fields
230
+ const handleTabNavigation = (currentIndex: number) => {
231
+ // console.log('handleTabNavigation called with index:', currentIndex);
232
+ const nextIndex = (currentIndex + 1) % words.length;
233
+ // console.log('Next index calculated:', nextIndex, 'Total words:', words.length);
234
+
235
+ // If the next word is on a different page, change the page
236
+ if (isGlobal && (nextIndex < startIndex || nextIndex >= endIndex)) {
237
+ // console.log('Next word is on different page. Current page:', page, 'startIndex:', startIndex, 'endIndex:', endIndex);
238
+ const nextPage = Math.floor(nextIndex / pageSize) + 1;
239
+ // console.log('Changing to page:', nextPage);
240
+ setPage(nextPage);
241
+
242
+ // Use setTimeout to allow the page change to render before focusing
243
+ setTimeout(() => {
244
+ // console.log('Timeout callback executing, trying to focus element with ID:', `word-text-${nextIndex}`);
245
+ focusWordTextField(nextIndex);
246
+ }, 50);
247
+ } else {
248
+ // console.log('Next word is on same page, trying to focus element with ID:', `word-text-${nextIndex}`);
249
+ focusWordTextField(nextIndex);
250
+ }
251
+ };
252
+
253
+ // Helper function to focus a word text field by index
254
+ const focusWordTextField = (index: number) => {
255
+ // Material-UI TextField uses a more complex structure
256
+ // The actual input is inside the TextField component
257
+ const element = document.getElementById(`word-text-${index}`);
258
+ // console.log('Element found:', !!element);
259
+
260
+ if (element) {
261
+ // Try different selectors to find the input element
262
+ // First try the standard input selector
263
+ let input = element.querySelector('input');
264
+
265
+ // If that doesn't work, try the MUI-specific selector
266
+ if (!input) {
267
+ input = element.querySelector('.MuiInputBase-input');
268
+ }
269
+
270
+ // console.log('Input element found:', !!input);
271
+ if (input) {
272
+ input.focus();
273
+ input.select();
274
+ // console.log('Focus and select called on input');
275
+ } else {
276
+ // As a fallback, try to focus the TextField itself
277
+ // console.log('Trying to focus the TextField itself');
278
+ element.focus();
279
+ }
280
+ }
281
+ };
282
+
213
283
  return (
214
284
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flexGrow: 1, minHeight: 0 }}>
215
285
  {/* Initial divider with Add Segment Before button */}
@@ -265,6 +335,7 @@ export default function EditWordList({
265
335
  onMergeSegment={onMergeSegment}
266
336
  wordsLength={words.length}
267
337
  isGlobal={isGlobal}
338
+ onTabNavigation={handleTabNavigation}
268
339
  />
269
340
  );
270
341
  })}
@@ -1,14 +1,12 @@
1
1
  import { Box, Button, Typography, useMediaQuery, useTheme, Switch, FormControlLabel, Tooltip, Paper } from '@mui/material'
2
2
  import LockIcon from '@mui/icons-material/Lock'
3
3
  import UploadFileIcon from '@mui/icons-material/UploadFile'
4
- import TextSnippetIcon from '@mui/icons-material/TextSnippet'
5
4
  import FindReplaceIcon from '@mui/icons-material/FindReplace'
6
5
  import EditIcon from '@mui/icons-material/Edit'
7
- import { CorrectionData } from '../types'
6
+ import { CorrectionData, InteractionMode } from '../types'
8
7
  import CorrectionMetrics from './CorrectionMetrics'
9
8
  import ModeSelector from './ModeSelector'
10
9
  import AudioPlayer from './AudioPlayer'
11
- import { InteractionMode } from '../types'
12
10
  import { ApiClient } from '../api'
13
11
  import { findWordById } from './shared/utils/wordUtils'
14
12
 
@@ -29,7 +27,6 @@ interface HeaderProps {
29
27
  onHandlerToggle: (handler: string, enabled: boolean) => void
30
28
  isUpdatingHandlers: boolean
31
29
  onHandlerClick?: (handler: string) => void
32
- onAddLyrics?: () => void
33
30
  onFindReplace?: () => void
34
31
  onEditAll?: () => void
35
32
  }
@@ -47,7 +44,6 @@ export default function Header({
47
44
  onHandlerToggle,
48
45
  isUpdatingHandlers,
49
46
  onHandlerClick,
50
- onAddLyrics,
51
47
  onFindReplace,
52
48
  onEditAll,
53
49
  }: HeaderProps) {
@@ -271,17 +267,6 @@ export default function Header({
271
267
  audioHash={audioHash}
272
268
  />
273
269
  </Box>
274
- {!isReadOnly && apiClient && onAddLyrics && (
275
- <Button
276
- variant="outlined"
277
- size="small"
278
- onClick={onAddLyrics}
279
- startIcon={<TextSnippetIcon />}
280
- sx={{ minWidth: 'fit-content', height: '32px' }}
281
- >
282
- Add Reference Lyrics
283
- </Button>
284
- )}
285
270
  </Box>
286
271
  </Paper>
287
272
  </>
@@ -22,7 +22,8 @@ import {
22
22
  deleteSegment,
23
23
  updateSegment,
24
24
  mergeSegment,
25
- findAndReplace
25
+ findAndReplace,
26
+ deleteWord
26
27
  } from './shared/utils/segmentOperations'
27
28
  import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
28
29
  import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
@@ -124,6 +125,7 @@ interface MemoizedReferenceViewProps {
124
125
  onSourceChange: (source: string) => void
125
126
  corrected_segments: LyricsSegment[]
126
127
  corrections: WordCorrection[]
128
+ onAddLyrics?: () => void
127
129
  }
128
130
 
129
131
  // Create a memoized ReferenceView component
@@ -139,7 +141,8 @@ const MemoizedReferenceView = memo(function MemoizedReferenceView({
139
141
  currentSource,
140
142
  onSourceChange,
141
143
  corrected_segments,
142
- corrections
144
+ corrections,
145
+ onAddLyrics
143
146
  }: MemoizedReferenceViewProps) {
144
147
  return (
145
148
  <ReferenceView
@@ -155,6 +158,7 @@ const MemoizedReferenceView = memo(function MemoizedReferenceView({
155
158
  onSourceChange={onSourceChange}
156
159
  corrected_segments={corrected_segments}
157
160
  corrections={corrections}
161
+ onAddLyrics={onAddLyrics}
158
162
  />
159
163
  );
160
164
  });
@@ -195,7 +199,6 @@ const MemoizedHeader = memo(function MemoizedHeader({
195
199
  onHandlerToggle,
196
200
  isUpdatingHandlers,
197
201
  onHandlerClick,
198
- onAddLyrics,
199
202
  onFindReplace,
200
203
  onEditAll
201
204
  }: MemoizedHeaderProps) {
@@ -213,7 +216,6 @@ const MemoizedHeader = memo(function MemoizedHeader({
213
216
  onHandlerToggle={onHandlerToggle}
214
217
  isUpdatingHandlers={isUpdatingHandlers}
215
218
  onHandlerClick={onHandlerClick}
216
- onAddLyrics={onAddLyrics}
217
219
  onFindReplace={onFindReplace}
218
220
  onEditAll={onEditAll}
219
221
  />
@@ -236,6 +238,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
236
238
  const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
237
239
  const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
238
240
  const [isShiftPressed, setIsShiftPressed] = useState(false)
241
+ const [isCtrlPressed, setIsCtrlPressed] = useState(false)
239
242
  const [editModalSegment, setEditModalSegment] = useState<{
240
243
  segment: LyricsSegment
241
244
  index: number
@@ -301,8 +304,9 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
301
304
  })
302
305
  }
303
306
 
304
- const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
307
+ const { handleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
305
308
  setIsShiftPressed,
309
+ setIsCtrlPressed
306
310
  })
307
311
 
308
312
  // Always add keyboard listeners
@@ -315,6 +319,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
315
319
  // Reset modifier states when a modal opens
316
320
  if (isAnyModalOpen) {
317
321
  setIsShiftPressed(false)
322
+ setIsCtrlPressed(false)
318
323
  }
319
324
 
320
325
  // Cleanup function
@@ -325,8 +330,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
325
330
  window.removeEventListener('keydown', handleKeyDown)
326
331
  window.removeEventListener('keyup', handleKeyUp)
327
332
  document.body.style.userSelect = ''
333
+ // Call the cleanup function to remove window blur/focus listeners
334
+ cleanup()
328
335
  }
329
- }, [setIsShiftPressed, isAnyModalOpen])
336
+ }, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen])
330
337
 
331
338
  // Update modal state tracking
332
339
  useEffect(() => {
@@ -342,7 +349,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
342
349
  }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isEditAllModalOpen])
343
350
 
344
351
  // Calculate effective mode based on modifier key states
345
- const effectiveMode = isShiftPressed ? 'highlight' : interactionMode
352
+ const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
346
353
 
347
354
  const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
348
355
  setFlashingType(null)
@@ -367,6 +374,16 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
367
374
  console.log('LyricsAnalyzer handleWordClick:', { info });
368
375
  }
369
376
 
377
+ if (effectiveMode === 'delete_word') {
378
+ // Use the shared deleteWord utility function
379
+ const newData = deleteWord(data, info.word_id);
380
+ setData(newData);
381
+
382
+ // Flash to indicate the word was deleted
383
+ handleFlash('word');
384
+ return;
385
+ }
386
+
370
387
  if (effectiveMode === 'highlight') {
371
388
  // Find if this word is part of a correction
372
389
  const correction = data.corrections?.find(c =>
@@ -491,7 +508,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
491
508
  });
492
509
  }
493
510
  }
494
- }, [data, effectiveMode, setModalContent]);
511
+ }, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
495
512
 
496
513
  const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
497
514
  if (!editModalSegment) return
@@ -948,6 +965,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
948
965
  onSourceChange={setCurrentSource}
949
966
  corrected_segments={data.corrected_segments}
950
967
  corrections={data.corrections}
968
+ onAddLyrics={() => setIsAddLyricsModalOpen(true)}
951
969
  />
952
970
  </Grid>
953
971
  </Grid>
@@ -1,6 +1,7 @@
1
- import { ToggleButton, ToggleButtonGroup, Box, Typography } from '@mui/material';
1
+ import { ToggleButton, ToggleButtonGroup, Box, Typography, Tooltip } from '@mui/material';
2
2
  import HighlightIcon from '@mui/icons-material/Highlight';
3
3
  import EditIcon from '@mui/icons-material/Edit';
4
+ import DeleteIcon from '@mui/icons-material/Delete';
4
5
  import { InteractionMode } from '../types';
5
6
 
6
7
  interface ModeSelectorProps {
@@ -17,7 +18,7 @@ export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorPr
17
18
  <ToggleButtonGroup
18
19
  value={effectiveMode}
19
20
  exclusive
20
- onChange={(_, newMode) => newMode && onChange(newMode)}
21
+ onChange={(_, newMode) => newMode === 'edit' && onChange(newMode)}
21
22
  size="small"
22
23
  sx={{
23
24
  height: '32px',
@@ -28,20 +29,38 @@ export default function ModeSelector({ effectiveMode, onChange }: ModeSelectorPr
28
29
  }
29
30
  }}
30
31
  >
31
- <ToggleButton
32
- value="edit"
33
- title="Click to edit segments and make corrections in the transcription view"
34
- >
35
- <EditIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
36
- Edit
37
- </ToggleButton>
38
- <ToggleButton
39
- value="highlight"
40
- title="Click words in the transcription view to highlight the matching anchor sequence in the reference lyrics. You can also hold SHIFT to temporarily activate this mode."
41
- >
42
- <HighlightIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
43
- Highlight
44
- </ToggleButton>
32
+ <Tooltip title="Default mode; click words to edit that lyrics segment">
33
+ <ToggleButton
34
+ value="edit"
35
+ >
36
+ <EditIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
37
+ Edit
38
+ </ToggleButton>
39
+ </Tooltip>
40
+
41
+ <Tooltip title="Hold SHIFT and click words to highlight the matching anchor sequence in the reference lyrics">
42
+ <span>
43
+ <ToggleButton
44
+ value="highlight"
45
+ disabled
46
+ >
47
+ <HighlightIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
48
+ Highlight
49
+ </ToggleButton>
50
+ </span>
51
+ </Tooltip>
52
+
53
+ <Tooltip title="Hold CTRL and click words to delete them">
54
+ <span>
55
+ <ToggleButton
56
+ value="delete_word"
57
+ disabled
58
+ >
59
+ <DeleteIcon sx={{ mr: 0.5, fontSize: '1rem' }} />
60
+ Delete
61
+ </ToggleButton>
62
+ </span>
63
+ </Tooltip>
45
64
  </ToggleButtonGroup>
46
65
  </Box>
47
66
  );
@@ -34,7 +34,8 @@ export default function ReferenceView({
34
34
  highlightInfo,
35
35
  mode,
36
36
  gaps,
37
- corrections
37
+ corrections,
38
+ onAddLyrics
38
39
  }: ReferenceViewProps) {
39
40
  // Get available sources from referenceSources object
40
41
  const availableSources = useMemo(() =>
@@ -183,6 +184,7 @@ export default function ReferenceView({
183
184
  availableSources={availableSources}
184
185
  currentSource={effectiveCurrentSource}
185
186
  onSourceChange={onSourceChange}
187
+ onAddLyrics={onAddLyrics}
186
188
  />
187
189
  </Box>
188
190
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.2 }}>
@@ -181,7 +181,8 @@ export function HighlightedText({
181
181
  return correction ? {
182
182
  originalWord: correction.original_word,
183
183
  handler: correction.handler,
184
- confidence: correction.confidence
184
+ confidence: correction.confidence,
185
+ source: correction.source
185
186
  } : null;
186
187
  })()}
187
188
  />
@@ -219,7 +220,8 @@ export function HighlightedText({
219
220
  const correctionInfo = correction ? {
220
221
  originalWord: correction.original_word,
221
222
  handler: correction.handler,
222
- confidence: correction.confidence
223
+ confidence: correction.confidence,
224
+ source: correction.source
223
225
  } : null;
224
226
 
225
227
  return (
@@ -1,21 +1,23 @@
1
1
  import { Box, Button } from '@mui/material'
2
+ import AddIcon from '@mui/icons-material/Add'
2
3
 
3
4
  export interface SourceSelectorProps {
4
5
  currentSource: string
5
6
  onSourceChange: (source: string) => void
6
7
  availableSources: string[]
8
+ onAddLyrics?: () => void
7
9
  }
8
10
 
9
- export function SourceSelector({ currentSource, onSourceChange, availableSources }: SourceSelectorProps) {
11
+ export function SourceSelector({ currentSource, onSourceChange, availableSources, onAddLyrics }: SourceSelectorProps) {
10
12
  return (
11
- <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.3 }}>
13
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.3, alignItems: 'center' }}>
12
14
  {availableSources.map((source) => (
13
15
  <Button
14
16
  key={source}
15
17
  size="small"
16
18
  variant={currentSource === source ? 'contained' : 'outlined'}
17
19
  onClick={() => onSourceChange(source)}
18
- sx={{
20
+ sx={{
19
21
  mr: 0,
20
22
  py: 0.2,
21
23
  px: 0.8,
@@ -28,6 +30,27 @@ export function SourceSelector({ currentSource, onSourceChange, availableSources
28
30
  {source.charAt(0).toUpperCase() + source.slice(1)}
29
31
  </Button>
30
32
  ))}
33
+ {onAddLyrics && (
34
+ <Button
35
+ size="small"
36
+ variant="outlined"
37
+ onClick={onAddLyrics}
38
+ startIcon={<AddIcon sx={{ fontSize: '0.9rem' }} />}
39
+ sx={{
40
+ mr: 0,
41
+ py: 0.2,
42
+ px: 0.8,
43
+ minWidth: 'auto',
44
+ fontSize: '0.7rem',
45
+ lineHeight: 1.2,
46
+ '& .MuiButton-startIcon': {
47
+ marginLeft: '-5px',
48
+ marginRight: '1px',
49
+ marginTop: '-1px'
50
+ }
51
+ }}
52
+ >New</Button>
53
+ )}
31
54
  </Box>
32
55
  )
33
56
  }
@@ -61,7 +61,8 @@ export const WordComponent = React.memo(function Word({
61
61
  const tooltipContent = (
62
62
  <>
63
63
  <strong>Original:</strong> "{correction.originalWord}"<br />
64
- <strong>Corrected by:</strong> {correction.handler}
64
+ <strong>Corrected by:</strong> {correction.handler}<br />
65
+ <strong>Source:</strong> {correction.source}
65
66
  </>
66
67
  )
67
68
 
@@ -97,7 +97,7 @@ export function useWordClick({
97
97
  }
98
98
  }
99
99
 
100
- if (mode === 'highlight' || mode === 'edit') {
100
+ if (mode === 'highlight' || mode === 'edit' || mode === 'delete_word') {
101
101
  if (belongsToAnchor && anchor) {
102
102
  onWordClick?.({
103
103
  word_id: wordId,
@@ -131,7 +131,8 @@ export function useWordClick({
131
131
  gap: undefined
132
132
  })
133
133
  }
134
- } else if (mode === 'details') {
134
+ } else {
135
+ // This is a fallback for any future modes
135
136
  if (belongsToAnchor && anchor) {
136
137
  onElementClick({
137
138
  type: 'anchor',