lyrics-transcriber 0.43.1__py3-none-any.whl → 0.44.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.
- lyrics_transcriber/core/controller.py +58 -24
- lyrics_transcriber/correction/anchor_sequence.py +22 -8
- lyrics_transcriber/correction/corrector.py +47 -3
- lyrics_transcriber/correction/handlers/llm.py +15 -12
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
- lyrics_transcriber/frontend/dist/assets/{index-D0Gr3Ep7.js → index-DVoI6Z16.js} +10799 -7490
- lyrics_transcriber/frontend/dist/assets/index-DVoI6Z16.js.map +1 -0
- lyrics_transcriber/frontend/dist/index.html +1 -1
- lyrics_transcriber/frontend/src/App.tsx +4 -4
- lyrics_transcriber/frontend/src/api.ts +37 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +14 -10
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +62 -56
- lyrics_transcriber/frontend/src/components/EditModal.tsx +232 -237
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/GlobalSyncEditor.tsx +675 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +141 -101
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +146 -80
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +22 -13
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +1 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +29 -12
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +21 -4
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -15
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +34 -16
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +186 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +89 -41
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +9 -2
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +28 -3
- lyrics_transcriber/frontend/src/components/shared/types.ts +17 -2
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +63 -14
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +192 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +267 -0
- lyrics_transcriber/frontend/src/main.tsx +7 -1
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types.ts +1 -1
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/lyrics/base_lyrics_provider.py +2 -2
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/generator.py +40 -12
- lyrics_transcriber/review/server.py +238 -8
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/METADATA +3 -2
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/RECORD +46 -40
- lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +0 -1
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +0 -252
- lyrics_transcriber/frontend/src/components/WordEditControls.tsx +0 -110
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.43.1.dist-info → lyrics_transcriber-0.44.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,467 @@
|
|
1
|
+
import {
|
2
|
+
Dialog,
|
3
|
+
DialogTitle,
|
4
|
+
DialogContent,
|
5
|
+
DialogActions,
|
6
|
+
IconButton,
|
7
|
+
Box,
|
8
|
+
TextField,
|
9
|
+
Button,
|
10
|
+
Typography,
|
11
|
+
Switch,
|
12
|
+
FormControlLabel,
|
13
|
+
Divider,
|
14
|
+
List,
|
15
|
+
ListItem,
|
16
|
+
ListItemText,
|
17
|
+
Paper,
|
18
|
+
Alert,
|
19
|
+
Tooltip,
|
20
|
+
CircularProgress
|
21
|
+
} from '@mui/material'
|
22
|
+
import CloseIcon from '@mui/icons-material/Close'
|
23
|
+
import { useState, useEffect } from 'react'
|
24
|
+
import { CorrectionData } from '../types'
|
25
|
+
|
26
|
+
interface MatchPreview {
|
27
|
+
segmentIndex: number
|
28
|
+
wordIndex?: number // Optional for full text mode
|
29
|
+
wordIndices?: number[] // For full text mode with multiple words
|
30
|
+
segmentText: string
|
31
|
+
wordText: string
|
32
|
+
replacement: string
|
33
|
+
willBeRemoved: boolean
|
34
|
+
isMultiWord?: boolean
|
35
|
+
}
|
36
|
+
|
37
|
+
interface FindReplaceModalProps {
|
38
|
+
open: boolean
|
39
|
+
onClose: () => void
|
40
|
+
onReplace: (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => void
|
41
|
+
data: CorrectionData
|
42
|
+
}
|
43
|
+
|
44
|
+
export default function FindReplaceModal({
|
45
|
+
open,
|
46
|
+
onClose,
|
47
|
+
onReplace,
|
48
|
+
data
|
49
|
+
}: FindReplaceModalProps) {
|
50
|
+
const [findText, setFindText] = useState('')
|
51
|
+
const [replaceText, setReplaceText] = useState('')
|
52
|
+
const [caseSensitive, setCaseSensitive] = useState(false)
|
53
|
+
const [useRegex, setUseRegex] = useState(false)
|
54
|
+
const [fullTextMode, setFullTextMode] = useState(false)
|
55
|
+
const [matchPreviews, setMatchPreviews] = useState<MatchPreview[]>([])
|
56
|
+
const [regexError, setRegexError] = useState<string | null>(null)
|
57
|
+
const [isSearching, setIsSearching] = useState(false)
|
58
|
+
const [hasEmptyReplacements, setHasEmptyReplacements] = useState(false)
|
59
|
+
|
60
|
+
// Reset state when modal opens
|
61
|
+
useEffect(() => {
|
62
|
+
if (open) {
|
63
|
+
setMatchPreviews([])
|
64
|
+
setRegexError(null)
|
65
|
+
setHasEmptyReplacements(false)
|
66
|
+
}
|
67
|
+
}, [open])
|
68
|
+
|
69
|
+
// Find matches whenever search parameters change
|
70
|
+
useEffect(() => {
|
71
|
+
if (!open || !findText) {
|
72
|
+
setMatchPreviews([])
|
73
|
+
setRegexError(null)
|
74
|
+
setHasEmptyReplacements(false)
|
75
|
+
return
|
76
|
+
}
|
77
|
+
|
78
|
+
setIsSearching(true)
|
79
|
+
|
80
|
+
// Use setTimeout to prevent UI freezing for large datasets
|
81
|
+
const timeoutId = setTimeout(() => {
|
82
|
+
try {
|
83
|
+
const matches = fullTextMode
|
84
|
+
? findMatchesFullText(data, findText, replaceText, { caseSensitive, useRegex })
|
85
|
+
: findMatches(data, findText, replaceText, { caseSensitive, useRegex });
|
86
|
+
|
87
|
+
setMatchPreviews(matches)
|
88
|
+
|
89
|
+
// Check if any replacements would result in empty words
|
90
|
+
const hasEmpty = matches.some(match => match.willBeRemoved)
|
91
|
+
setHasEmptyReplacements(hasEmpty)
|
92
|
+
|
93
|
+
setRegexError(null)
|
94
|
+
} catch (error) {
|
95
|
+
if (error instanceof Error) {
|
96
|
+
setRegexError(error.message)
|
97
|
+
} else {
|
98
|
+
setRegexError('Invalid regex pattern')
|
99
|
+
}
|
100
|
+
setMatchPreviews([])
|
101
|
+
setHasEmptyReplacements(false)
|
102
|
+
} finally {
|
103
|
+
setIsSearching(false)
|
104
|
+
}
|
105
|
+
}, 300)
|
106
|
+
|
107
|
+
return () => clearTimeout(timeoutId)
|
108
|
+
}, [open, data, findText, replaceText, caseSensitive, useRegex, fullTextMode])
|
109
|
+
|
110
|
+
const handleReplace = () => {
|
111
|
+
if (!findText || regexError) return
|
112
|
+
onReplace(findText, replaceText, { caseSensitive, useRegex, fullTextMode })
|
113
|
+
onClose()
|
114
|
+
}
|
115
|
+
|
116
|
+
const handleKeyDown = (event: React.KeyboardEvent) => {
|
117
|
+
if (event.key === 'Enter' && !event.shiftKey && !regexError && findText) {
|
118
|
+
event.preventDefault()
|
119
|
+
handleReplace()
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
// Function to find matches at the word level (original implementation)
|
124
|
+
const findMatches = (
|
125
|
+
data: CorrectionData,
|
126
|
+
findText: string,
|
127
|
+
replaceText: string,
|
128
|
+
options: { caseSensitive: boolean, useRegex: boolean }
|
129
|
+
): MatchPreview[] => {
|
130
|
+
const matches: MatchPreview[] = []
|
131
|
+
|
132
|
+
if (!findText) return matches
|
133
|
+
|
134
|
+
try {
|
135
|
+
const segments = data.corrected_segments || []
|
136
|
+
|
137
|
+
segments.forEach((segment, segmentIndex) => {
|
138
|
+
segment.words.forEach((word, wordIndex) => {
|
139
|
+
let pattern: RegExp
|
140
|
+
const replacement = replaceText
|
141
|
+
|
142
|
+
if (options.useRegex) {
|
143
|
+
// Create regex with or without case sensitivity
|
144
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
145
|
+
} else {
|
146
|
+
// Escape special regex characters for literal search
|
147
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
148
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
149
|
+
}
|
150
|
+
|
151
|
+
// Check if there's a match
|
152
|
+
if (pattern.test(word.text)) {
|
153
|
+
// Reset regex lastIndex
|
154
|
+
pattern.lastIndex = 0
|
155
|
+
|
156
|
+
// Create replacement preview
|
157
|
+
const replacedText = word.text.replace(pattern, replacement)
|
158
|
+
const willBeRemoved = replacedText.trim() === ''
|
159
|
+
|
160
|
+
matches.push({
|
161
|
+
segmentIndex,
|
162
|
+
wordIndex,
|
163
|
+
segmentText: segment.text,
|
164
|
+
wordText: word.text,
|
165
|
+
replacement: replacedText,
|
166
|
+
willBeRemoved
|
167
|
+
})
|
168
|
+
}
|
169
|
+
})
|
170
|
+
})
|
171
|
+
|
172
|
+
return matches
|
173
|
+
} catch (error) {
|
174
|
+
if (options.useRegex) {
|
175
|
+
throw new Error('Invalid regex pattern')
|
176
|
+
}
|
177
|
+
throw error
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
// Function to find matches across word boundaries (full text mode)
|
182
|
+
const findMatchesFullText = (
|
183
|
+
data: CorrectionData,
|
184
|
+
findText: string,
|
185
|
+
replaceText: string,
|
186
|
+
options: { caseSensitive: boolean, useRegex: boolean }
|
187
|
+
): MatchPreview[] => {
|
188
|
+
const matches: MatchPreview[] = []
|
189
|
+
|
190
|
+
if (!findText) return matches
|
191
|
+
|
192
|
+
try {
|
193
|
+
const segments = data.corrected_segments || []
|
194
|
+
|
195
|
+
segments.forEach((segment, segmentIndex) => {
|
196
|
+
let pattern: RegExp
|
197
|
+
|
198
|
+
if (options.useRegex) {
|
199
|
+
// Create regex with or without case sensitivity
|
200
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
201
|
+
} else {
|
202
|
+
// Escape special regex characters for literal search
|
203
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
204
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
205
|
+
}
|
206
|
+
|
207
|
+
// Get the full segment text
|
208
|
+
const segmentText = segment.text;
|
209
|
+
|
210
|
+
// Find all matches in the segment text
|
211
|
+
let match;
|
212
|
+
while ((match = pattern.exec(segmentText)) !== null) {
|
213
|
+
const matchText = match[0];
|
214
|
+
const startIndex = match.index;
|
215
|
+
const endIndex = startIndex + matchText.length;
|
216
|
+
|
217
|
+
// Find which words are affected by this match
|
218
|
+
const affectedWordIndices: number[] = [];
|
219
|
+
let startWordIndex = -1;
|
220
|
+
let currentPosition = 0;
|
221
|
+
|
222
|
+
for (let i = 0; i < segment.words.length; i++) {
|
223
|
+
const word = segment.words[i];
|
224
|
+
const wordStart = currentPosition;
|
225
|
+
const wordEnd = wordStart + word.text.length;
|
226
|
+
|
227
|
+
// Add spaces between words for position calculation
|
228
|
+
if (i > 0) currentPosition += 1;
|
229
|
+
|
230
|
+
// Check if this word is part of the match
|
231
|
+
if (wordEnd > startIndex && wordStart < endIndex) {
|
232
|
+
affectedWordIndices.push(i);
|
233
|
+
if (startWordIndex === -1) startWordIndex = i;
|
234
|
+
}
|
235
|
+
|
236
|
+
currentPosition += word.text.length;
|
237
|
+
}
|
238
|
+
|
239
|
+
if (affectedWordIndices.length > 0) {
|
240
|
+
// Create replacement preview
|
241
|
+
const willBeRemoved = replaceText.trim() === '';
|
242
|
+
|
243
|
+
matches.push({
|
244
|
+
segmentIndex,
|
245
|
+
wordIndices: affectedWordIndices,
|
246
|
+
segmentText,
|
247
|
+
wordText: matchText,
|
248
|
+
replacement: replaceText,
|
249
|
+
willBeRemoved,
|
250
|
+
isMultiWord: affectedWordIndices.length > 1
|
251
|
+
});
|
252
|
+
}
|
253
|
+
}
|
254
|
+
})
|
255
|
+
|
256
|
+
return matches
|
257
|
+
} catch (error) {
|
258
|
+
if (options.useRegex) {
|
259
|
+
throw new Error('Invalid regex pattern')
|
260
|
+
}
|
261
|
+
throw error
|
262
|
+
}
|
263
|
+
}
|
264
|
+
|
265
|
+
const getContextualMatch = (preview: MatchPreview) => {
|
266
|
+
if (preview.isMultiWord && preview.wordIndices) {
|
267
|
+
// For multi-word matches in full text mode
|
268
|
+
const segment = data.corrected_segments[preview.segmentIndex];
|
269
|
+
const words = segment.words;
|
270
|
+
|
271
|
+
// Get context words before and after the match
|
272
|
+
const firstMatchedWordIdx = preview.wordIndices[0];
|
273
|
+
const lastMatchedWordIdx = preview.wordIndices[preview.wordIndices.length - 1];
|
274
|
+
|
275
|
+
const startContextIdx = Math.max(0, firstMatchedWordIdx - 2);
|
276
|
+
const endContextIdx = Math.min(words.length - 1, lastMatchedWordIdx + 2);
|
277
|
+
|
278
|
+
const beforeWords = words.slice(startContextIdx, firstMatchedWordIdx).map(w => w.text).join(' ');
|
279
|
+
const matchedWords = words.slice(firstMatchedWordIdx, lastMatchedWordIdx + 1).map(w => w.text).join(' ');
|
280
|
+
const afterWords = words.slice(lastMatchedWordIdx + 1, endContextIdx + 1).map(w => w.text).join(' ');
|
281
|
+
|
282
|
+
return (
|
283
|
+
<Box>
|
284
|
+
{beforeWords && <Typography component="span" color="text.secondary">{beforeWords} </Typography>}
|
285
|
+
<Typography component="span" color="error" fontWeight="bold">{matchedWords}</Typography>
|
286
|
+
{afterWords && <Typography component="span" color="text.secondary"> {afterWords}</Typography>}
|
287
|
+
<Typography variant="body2" color="primary" sx={{ mt: 0.5 }}>
|
288
|
+
{preview.willBeRemoved ? (
|
289
|
+
<Typography component="span" color="warning.main" fontWeight="bold">
|
290
|
+
↳ Text will be removed
|
291
|
+
</Typography>
|
292
|
+
) : (
|
293
|
+
<>↳ <b>{preview.replacement}</b></>
|
294
|
+
)}
|
295
|
+
</Typography>
|
296
|
+
</Box>
|
297
|
+
);
|
298
|
+
} else {
|
299
|
+
// For single word matches (original implementation)
|
300
|
+
const words = data.corrected_segments[preview.segmentIndex].words;
|
301
|
+
const wordIndex = preview.wordIndex || 0;
|
302
|
+
|
303
|
+
// Get a few words before and after for context
|
304
|
+
const startIdx = Math.max(0, wordIndex - 2);
|
305
|
+
const endIdx = Math.min(words.length - 1, wordIndex + 2);
|
306
|
+
|
307
|
+
const beforeWords = words.slice(startIdx, wordIndex).map(w => w.text).join(' ');
|
308
|
+
const afterWords = words.slice(wordIndex + 1, endIdx + 1).map(w => w.text).join(' ');
|
309
|
+
|
310
|
+
return (
|
311
|
+
<Box>
|
312
|
+
{beforeWords && <Typography component="span" color="text.secondary">{beforeWords} </Typography>}
|
313
|
+
<Typography component="span" color="error" fontWeight="bold">{preview.wordText}</Typography>
|
314
|
+
{afterWords && <Typography component="span" color="text.secondary"> {afterWords}</Typography>}
|
315
|
+
<Typography variant="body2" color="primary" sx={{ mt: 0.5 }}>
|
316
|
+
{preview.willBeRemoved ? (
|
317
|
+
<Typography component="span" color="warning.main" fontWeight="bold">
|
318
|
+
↳ Word will be removed
|
319
|
+
</Typography>
|
320
|
+
) : (
|
321
|
+
<>↳ <b>{preview.replacement}</b></>
|
322
|
+
)}
|
323
|
+
</Typography>
|
324
|
+
</Box>
|
325
|
+
);
|
326
|
+
}
|
327
|
+
}
|
328
|
+
|
329
|
+
return (
|
330
|
+
<Dialog
|
331
|
+
open={open}
|
332
|
+
onClose={onClose}
|
333
|
+
maxWidth="md"
|
334
|
+
fullWidth
|
335
|
+
onKeyDown={handleKeyDown}
|
336
|
+
>
|
337
|
+
<DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
338
|
+
<Box sx={{ flex: 1 }}>Find and Replace</Box>
|
339
|
+
<IconButton onClick={onClose}>
|
340
|
+
<CloseIcon />
|
341
|
+
</IconButton>
|
342
|
+
</DialogTitle>
|
343
|
+
<DialogContent dividers>
|
344
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
345
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
346
|
+
<TextField
|
347
|
+
label="Find"
|
348
|
+
value={findText}
|
349
|
+
onChange={(e) => setFindText(e.target.value)}
|
350
|
+
fullWidth
|
351
|
+
size="small"
|
352
|
+
autoFocus
|
353
|
+
error={!!regexError}
|
354
|
+
helperText={regexError}
|
355
|
+
/>
|
356
|
+
<TextField
|
357
|
+
label="Replace with"
|
358
|
+
value={replaceText}
|
359
|
+
onChange={(e) => setReplaceText(e.target.value)}
|
360
|
+
fullWidth
|
361
|
+
size="small"
|
362
|
+
/>
|
363
|
+
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
364
|
+
<FormControlLabel
|
365
|
+
control={
|
366
|
+
<Switch
|
367
|
+
checked={caseSensitive}
|
368
|
+
onChange={(e) => setCaseSensitive(e.target.checked)}
|
369
|
+
/>
|
370
|
+
}
|
371
|
+
label="Case sensitive"
|
372
|
+
/>
|
373
|
+
<FormControlLabel
|
374
|
+
control={
|
375
|
+
<Switch
|
376
|
+
checked={useRegex}
|
377
|
+
onChange={(e) => setUseRegex(e.target.checked)}
|
378
|
+
/>
|
379
|
+
}
|
380
|
+
label={
|
381
|
+
<Tooltip title="Use JavaScript regular expressions for advanced pattern matching">
|
382
|
+
<span>Use regex</span>
|
383
|
+
</Tooltip>
|
384
|
+
}
|
385
|
+
/>
|
386
|
+
<FormControlLabel
|
387
|
+
control={
|
388
|
+
<Switch
|
389
|
+
checked={fullTextMode}
|
390
|
+
onChange={(e) => setFullTextMode(e.target.checked)}
|
391
|
+
/>
|
392
|
+
}
|
393
|
+
label={
|
394
|
+
<Tooltip title="Search across word boundaries to find and replace text that spans multiple words">
|
395
|
+
<span>Full text mode</span>
|
396
|
+
</Tooltip>
|
397
|
+
}
|
398
|
+
/>
|
399
|
+
</Box>
|
400
|
+
</Box>
|
401
|
+
|
402
|
+
<Divider />
|
403
|
+
|
404
|
+
<Box>
|
405
|
+
<Typography variant="subtitle1" gutterBottom>
|
406
|
+
Preview {isSearching ? <CircularProgress size={16} sx={{ ml: 1 }} /> : null}
|
407
|
+
</Typography>
|
408
|
+
|
409
|
+
{hasEmptyReplacements && (
|
410
|
+
<Alert severity="warning" sx={{ mb: 2 }}>
|
411
|
+
Some replacements will result in empty words, which will be removed.
|
412
|
+
</Alert>
|
413
|
+
)}
|
414
|
+
|
415
|
+
{fullTextMode && (
|
416
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
417
|
+
Full text mode is enabled. Matches can span across multiple words.
|
418
|
+
</Alert>
|
419
|
+
)}
|
420
|
+
|
421
|
+
{!isSearching && findText && matchPreviews.length === 0 && !regexError && (
|
422
|
+
<Alert severity="info">No matches found</Alert>
|
423
|
+
)}
|
424
|
+
|
425
|
+
{!isSearching && matchPreviews.length > 0 && (
|
426
|
+
<>
|
427
|
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
428
|
+
{matchPreviews.length} {matchPreviews.length === 1 ? 'match' : 'matches'} found
|
429
|
+
</Typography>
|
430
|
+
<Paper variant="outlined" sx={{ maxHeight: 300, overflow: 'auto' }}>
|
431
|
+
<List dense>
|
432
|
+
{matchPreviews.slice(0, 50).map((preview, index) => (
|
433
|
+
<ListItem key={index} divider={index < matchPreviews.length - 1}>
|
434
|
+
<ListItemText
|
435
|
+
primary={getContextualMatch(preview)}
|
436
|
+
secondary={`Segment ${preview.segmentIndex + 1}${preview.isMultiWord ? ' (spans multiple words)' : ''}`}
|
437
|
+
/>
|
438
|
+
</ListItem>
|
439
|
+
))}
|
440
|
+
{matchPreviews.length > 50 && (
|
441
|
+
<ListItem>
|
442
|
+
<ListItemText
|
443
|
+
primary={`${matchPreviews.length - 50} more matches not shown`}
|
444
|
+
primaryTypographyProps={{ color: 'text.secondary' }}
|
445
|
+
/>
|
446
|
+
</ListItem>
|
447
|
+
)}
|
448
|
+
</List>
|
449
|
+
</Paper>
|
450
|
+
</>
|
451
|
+
)}
|
452
|
+
</Box>
|
453
|
+
</Box>
|
454
|
+
</DialogContent>
|
455
|
+
<DialogActions>
|
456
|
+
<Button onClick={onClose}>Cancel</Button>
|
457
|
+
<Button
|
458
|
+
onClick={handleReplace}
|
459
|
+
disabled={!findText || !!regexError || matchPreviews.length === 0}
|
460
|
+
variant="contained"
|
461
|
+
>
|
462
|
+
Replace All ({matchPreviews.length})
|
463
|
+
</Button>
|
464
|
+
</DialogActions>
|
465
|
+
</Dialog>
|
466
|
+
)
|
467
|
+
}
|