lyrics-transcriber 0.34.0__py3-none-any.whl → 0.34.2__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/correction/handlers/syllables_match.py +22 -2
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +245 -0
- lyrics_transcriber/frontend/dist/index.html +13 -0
- lyrics_transcriber/frontend/dist/vite.svg +1 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +13 -0
- lyrics_transcriber/frontend/package-lock.json +4260 -0
- lyrics_transcriber/frontend/package.json +37 -0
- lyrics_transcriber/frontend/public/vite.svg +1 -0
- lyrics_transcriber/frontend/src/App.tsx +192 -0
- lyrics_transcriber/frontend/src/api.ts +59 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +155 -0
- lyrics_transcriber/frontend/src/components/DebugPanel.tsx +311 -0
- lyrics_transcriber/frontend/src/components/DetailsModal.tsx +297 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +450 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +287 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +157 -0
- lyrics_transcriber/frontend/src/components/constants.ts +19 -0
- lyrics_transcriber/frontend/src/components/styles.ts +13 -0
- lyrics_transcriber/frontend/src/main.tsx +6 -0
- lyrics_transcriber/frontend/src/types.ts +158 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +6 -0
- lyrics_transcriber/frontend/vite.config.ts +7 -0
- lyrics_transcriber/review/server.py +18 -29
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/RECORD +38 -7
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
{
|
2
|
+
"name": "lyrics-transcriber-frontend",
|
3
|
+
"private": true,
|
4
|
+
"homepage": "https://nomadkaraoke.github.io/lyrics-transcriber-frontend",
|
5
|
+
"version": "0.0.0",
|
6
|
+
"type": "module",
|
7
|
+
"scripts": {
|
8
|
+
"dev": "vite",
|
9
|
+
"build": "tsc -b && vite build",
|
10
|
+
"lint": "eslint .",
|
11
|
+
"preview": "vite preview",
|
12
|
+
"predeploy": "npm run build",
|
13
|
+
"deploy": "gh-pages -d dist"
|
14
|
+
},
|
15
|
+
"dependencies": {
|
16
|
+
"@emotion/react": "^11.14.0",
|
17
|
+
"@emotion/styled": "^11.14.0",
|
18
|
+
"@mui/icons-material": "^6.3.0",
|
19
|
+
"@mui/material": "^6.3.0",
|
20
|
+
"react": "^18.3.1",
|
21
|
+
"react-dom": "^18.3.1"
|
22
|
+
},
|
23
|
+
"devDependencies": {
|
24
|
+
"@eslint/js": "^9.17.0",
|
25
|
+
"@types/react": "^18.3.18",
|
26
|
+
"@types/react-dom": "^18.3.5",
|
27
|
+
"@vitejs/plugin-react": "^4.3.4",
|
28
|
+
"eslint": "^9.17.0",
|
29
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
30
|
+
"eslint-plugin-react-refresh": "^0.4.16",
|
31
|
+
"gh-pages": "^6.3.0",
|
32
|
+
"globals": "^15.14.0",
|
33
|
+
"typescript": "~5.6.2",
|
34
|
+
"typescript-eslint": "^8.18.2",
|
35
|
+
"vite": "^6.0.5"
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
@@ -0,0 +1,192 @@
|
|
1
|
+
import UploadFileIcon from '@mui/icons-material/UploadFile'
|
2
|
+
import { Alert, Box, Button, Modal, Typography } from '@mui/material'
|
3
|
+
import { useEffect, useState } from 'react'
|
4
|
+
import { ApiClient, FileOnlyClient, LiveApiClient } from './api'
|
5
|
+
import CorrectionMetrics from './components/CorrectionMetrics'
|
6
|
+
import LyricsAnalyzer from './components/LyricsAnalyzer'
|
7
|
+
import { CorrectionData } from './types'
|
8
|
+
|
9
|
+
export default function App() {
|
10
|
+
const [data, setData] = useState<CorrectionData | null>(null)
|
11
|
+
const [showMetadata, setShowMetadata] = useState(false)
|
12
|
+
const [error, setError] = useState<string | null>(null)
|
13
|
+
const [apiClient, setApiClient] = useState<ApiClient | null>(null)
|
14
|
+
const [isReadOnly, setIsReadOnly] = useState(true)
|
15
|
+
|
16
|
+
useEffect(() => {
|
17
|
+
// Parse query parameters
|
18
|
+
const params = new URLSearchParams(window.location.search)
|
19
|
+
const encodedApiUrl = params.get('baseApiUrl')
|
20
|
+
|
21
|
+
if (encodedApiUrl) {
|
22
|
+
const baseApiUrl = decodeURIComponent(encodedApiUrl)
|
23
|
+
setApiClient(new LiveApiClient(baseApiUrl))
|
24
|
+
setIsReadOnly(false)
|
25
|
+
// Fetch initial data
|
26
|
+
fetchData(baseApiUrl)
|
27
|
+
} else {
|
28
|
+
setApiClient(new FileOnlyClient())
|
29
|
+
setIsReadOnly(true)
|
30
|
+
}
|
31
|
+
}, [])
|
32
|
+
|
33
|
+
const fetchData = async (baseUrl: string) => {
|
34
|
+
try {
|
35
|
+
const client = new LiveApiClient(baseUrl)
|
36
|
+
const data = await client.getCorrectionData()
|
37
|
+
console.log('Fetched data:', data)
|
38
|
+
setData(data)
|
39
|
+
} catch (err) {
|
40
|
+
const error = err as Error
|
41
|
+
setError(`Failed to fetch data: ${error.message}`)
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
const handleFileLoad = async () => {
|
46
|
+
const input = document.createElement('input')
|
47
|
+
input.type = 'file'
|
48
|
+
input.accept = '.json'
|
49
|
+
|
50
|
+
input.onchange = async (e) => {
|
51
|
+
const file = (e.target as HTMLInputElement).files?.[0]
|
52
|
+
if (!file) return
|
53
|
+
|
54
|
+
try {
|
55
|
+
const text = await file.text()
|
56
|
+
console.log('File contents:', text.slice(0, 500) + '...') // Show first 500 chars
|
57
|
+
const parsedData = JSON.parse(text) as CorrectionData
|
58
|
+
console.log('Parsed file data:', parsedData)
|
59
|
+
setData(parsedData)
|
60
|
+
} catch (err) {
|
61
|
+
const error = err as Error
|
62
|
+
setError(`Error loading file: ${error.message}. Please make sure it is a valid JSON file.`)
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
input.click()
|
67
|
+
}
|
68
|
+
|
69
|
+
const renderMetadataModal = () => {
|
70
|
+
if (!data) return null
|
71
|
+
|
72
|
+
return (
|
73
|
+
<Modal
|
74
|
+
open={showMetadata}
|
75
|
+
onClose={() => setShowMetadata(false)}
|
76
|
+
aria-labelledby="metadata-modal"
|
77
|
+
>
|
78
|
+
<Box sx={{
|
79
|
+
position: 'absolute',
|
80
|
+
top: '50%',
|
81
|
+
left: '50%',
|
82
|
+
transform: 'translate(-50%, -50%)',
|
83
|
+
width: 400,
|
84
|
+
bgcolor: 'background.paper',
|
85
|
+
boxShadow: 24,
|
86
|
+
p: 4,
|
87
|
+
borderRadius: 1,
|
88
|
+
}}>
|
89
|
+
<Typography variant="h6" gutterBottom>
|
90
|
+
Correction Process Details
|
91
|
+
</Typography>
|
92
|
+
<Box sx={{ mb: 2 }}>
|
93
|
+
<Typography variant="subtitle2" color="text.secondary">
|
94
|
+
Total Words
|
95
|
+
</Typography>
|
96
|
+
<Typography>
|
97
|
+
{data.metadata.total_words}
|
98
|
+
</Typography>
|
99
|
+
</Box>
|
100
|
+
<Box sx={{ mb: 2 }}>
|
101
|
+
<Typography variant="subtitle2" color="text.secondary">
|
102
|
+
Gap Sequences
|
103
|
+
</Typography>
|
104
|
+
<Typography>
|
105
|
+
{data.metadata.gap_sequences_count}
|
106
|
+
</Typography>
|
107
|
+
</Box>
|
108
|
+
<Box sx={{ mb: 2 }}>
|
109
|
+
<Typography variant="subtitle2" color="text.secondary">
|
110
|
+
Corrections Made
|
111
|
+
</Typography>
|
112
|
+
<Typography>
|
113
|
+
{data.corrections_made}
|
114
|
+
</Typography>
|
115
|
+
</Box>
|
116
|
+
<Box sx={{ mb: 2 }}>
|
117
|
+
<Typography variant="subtitle2" color="text.secondary">
|
118
|
+
Correction Ratio
|
119
|
+
</Typography>
|
120
|
+
<Typography>
|
121
|
+
{(data.metadata.correction_ratio * 100).toFixed(1)}%
|
122
|
+
</Typography>
|
123
|
+
</Box>
|
124
|
+
{/* Add any other metadata fields that are available */}
|
125
|
+
</Box>
|
126
|
+
</Modal>
|
127
|
+
)
|
128
|
+
}
|
129
|
+
|
130
|
+
if (!data) {
|
131
|
+
return (
|
132
|
+
<Box sx={{ p: 3 }}>
|
133
|
+
{error && (
|
134
|
+
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
135
|
+
{error}
|
136
|
+
</Alert>
|
137
|
+
)}
|
138
|
+
{isReadOnly ? (
|
139
|
+
<>
|
140
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
141
|
+
Running in read-only mode. Connect to an API to enable editing.
|
142
|
+
</Alert>
|
143
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
144
|
+
<Typography variant="h4">
|
145
|
+
Lyrics Correction Review
|
146
|
+
</Typography>
|
147
|
+
<Button
|
148
|
+
variant="outlined"
|
149
|
+
startIcon={<UploadFileIcon />}
|
150
|
+
onClick={handleFileLoad}
|
151
|
+
>
|
152
|
+
Load File
|
153
|
+
</Button>
|
154
|
+
</Box>
|
155
|
+
<Box sx={{ mb: 3 }}>
|
156
|
+
<CorrectionMetrics />
|
157
|
+
</Box>
|
158
|
+
</>
|
159
|
+
) : (
|
160
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '50vh' }}>
|
161
|
+
<Typography variant="h6" color="text.secondary">
|
162
|
+
Loading Lyrics Correction Review...
|
163
|
+
</Typography>
|
164
|
+
</Box>
|
165
|
+
)}
|
166
|
+
</Box>
|
167
|
+
)
|
168
|
+
}
|
169
|
+
|
170
|
+
return (
|
171
|
+
<Box sx={{ p: 3 }}>
|
172
|
+
{error && (
|
173
|
+
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
174
|
+
{error}
|
175
|
+
</Alert>
|
176
|
+
)}
|
177
|
+
{isReadOnly && (
|
178
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
179
|
+
Running in read-only mode. Connect to an API to enable editing.
|
180
|
+
</Alert>
|
181
|
+
)}
|
182
|
+
<LyricsAnalyzer
|
183
|
+
data={data}
|
184
|
+
onFileLoad={handleFileLoad}
|
185
|
+
onShowMetadata={() => setShowMetadata(true)}
|
186
|
+
apiClient={apiClient}
|
187
|
+
isReadOnly={isReadOnly}
|
188
|
+
/>
|
189
|
+
{renderMetadataModal()}
|
190
|
+
</Box>
|
191
|
+
)
|
192
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
import { CorrectionData } from './types';
|
2
|
+
|
3
|
+
// New file to handle API communication
|
4
|
+
export interface ApiClient {
|
5
|
+
getCorrectionData: () => Promise<CorrectionData>;
|
6
|
+
submitCorrections: (data: CorrectionData) => Promise<void>;
|
7
|
+
}
|
8
|
+
|
9
|
+
// Add new interface for the minimal update payload
|
10
|
+
interface CorrectionUpdate {
|
11
|
+
corrections: CorrectionData['corrections'];
|
12
|
+
corrected_segments: CorrectionData['corrected_segments'];
|
13
|
+
}
|
14
|
+
|
15
|
+
export class LiveApiClient implements ApiClient {
|
16
|
+
constructor(private baseUrl: string) {
|
17
|
+
this.baseUrl = baseUrl.replace(/\/$/, '')
|
18
|
+
}
|
19
|
+
|
20
|
+
async getCorrectionData(): Promise<CorrectionData> {
|
21
|
+
const response = await fetch(`${this.baseUrl}/correction-data`);
|
22
|
+
if (!response.ok) {
|
23
|
+
throw new Error(`API error: ${response.statusText}`);
|
24
|
+
}
|
25
|
+
const data = await response.json();
|
26
|
+
return data;
|
27
|
+
}
|
28
|
+
|
29
|
+
async submitCorrections(data: CorrectionData): Promise<void> {
|
30
|
+
// Extract only the needed fields
|
31
|
+
const updatePayload: CorrectionUpdate = {
|
32
|
+
corrections: data.corrections,
|
33
|
+
corrected_segments: data.corrected_segments
|
34
|
+
};
|
35
|
+
|
36
|
+
const response = await fetch(`${this.baseUrl}/complete`, {
|
37
|
+
method: 'POST',
|
38
|
+
headers: {
|
39
|
+
'Content-Type': 'application/json',
|
40
|
+
},
|
41
|
+
body: JSON.stringify(updatePayload)
|
42
|
+
});
|
43
|
+
|
44
|
+
if (!response.ok) {
|
45
|
+
throw new Error(`API error: ${response.statusText}`);
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
export class FileOnlyClient implements ApiClient {
|
51
|
+
async getCorrectionData(): Promise<CorrectionData> {
|
52
|
+
throw new Error('Not supported in file-only mode');
|
53
|
+
}
|
54
|
+
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
56
|
+
async submitCorrections(_data: CorrectionData): Promise<void> {
|
57
|
+
throw new Error('Not supported in file-only mode');
|
58
|
+
}
|
59
|
+
}
|
@@ -0,0 +1,155 @@
|
|
1
|
+
import { Grid, Paper, Box, Typography } from '@mui/material'
|
2
|
+
import { COLORS } from './constants'
|
3
|
+
|
4
|
+
interface MetricProps {
|
5
|
+
color?: string
|
6
|
+
label: string
|
7
|
+
value: string | number
|
8
|
+
description: string
|
9
|
+
details?: Array<{ label: string, value: number }>
|
10
|
+
onClick?: () => void
|
11
|
+
}
|
12
|
+
|
13
|
+
function Metric({ color, label, value, description, details, onClick }: MetricProps) {
|
14
|
+
return (
|
15
|
+
<Paper
|
16
|
+
sx={{
|
17
|
+
p: 2,
|
18
|
+
cursor: onClick ? 'pointer' : 'default',
|
19
|
+
'&:hover': onClick ? {
|
20
|
+
bgcolor: 'action.hover'
|
21
|
+
} : undefined
|
22
|
+
}}
|
23
|
+
onClick={onClick}
|
24
|
+
>
|
25
|
+
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
|
26
|
+
{color && (
|
27
|
+
<Box
|
28
|
+
sx={{
|
29
|
+
width: 16,
|
30
|
+
height: 16,
|
31
|
+
borderRadius: 1,
|
32
|
+
bgcolor: color,
|
33
|
+
mr: 1,
|
34
|
+
}}
|
35
|
+
/>
|
36
|
+
)}
|
37
|
+
<Typography variant="subtitle2" color="text.secondary">
|
38
|
+
{label}
|
39
|
+
</Typography>
|
40
|
+
</Box>
|
41
|
+
<Typography variant="h6">
|
42
|
+
{value}
|
43
|
+
</Typography>
|
44
|
+
<Typography variant="caption" color="text.secondary">
|
45
|
+
{description}
|
46
|
+
</Typography>
|
47
|
+
{details && (
|
48
|
+
<Box sx={{ mt: 1, pt: 1, borderTop: 1, borderColor: 'divider' }}>
|
49
|
+
{details.map((detail, index) => (
|
50
|
+
<Box key={index} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 0.5 }}>
|
51
|
+
<Typography variant="caption" color="text.secondary">
|
52
|
+
{detail.label}
|
53
|
+
</Typography>
|
54
|
+
<Typography variant="caption">
|
55
|
+
{detail.value}
|
56
|
+
</Typography>
|
57
|
+
</Box>
|
58
|
+
))}
|
59
|
+
</Box>
|
60
|
+
)}
|
61
|
+
</Paper>
|
62
|
+
)
|
63
|
+
}
|
64
|
+
|
65
|
+
interface CorrectionMetricsProps {
|
66
|
+
// Anchor metrics
|
67
|
+
anchorCount?: number
|
68
|
+
multiSourceAnchors?: number
|
69
|
+
singleSourceMatches?: {
|
70
|
+
spotify: number
|
71
|
+
genius: number
|
72
|
+
}
|
73
|
+
// Gap metrics
|
74
|
+
correctedGapCount?: number
|
75
|
+
uncorrectedGapCount?: number
|
76
|
+
uncorrectedGaps?: Array<{
|
77
|
+
position: number
|
78
|
+
length: number
|
79
|
+
}>
|
80
|
+
// Correction details
|
81
|
+
replacedCount?: number
|
82
|
+
addedCount?: number
|
83
|
+
deletedCount?: number
|
84
|
+
onMetricClick?: {
|
85
|
+
anchor?: () => void
|
86
|
+
corrected?: () => void
|
87
|
+
uncorrected?: () => void
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
export default function CorrectionMetrics({
|
92
|
+
anchorCount,
|
93
|
+
multiSourceAnchors = 0,
|
94
|
+
singleSourceMatches = { spotify: 0, genius: 0 },
|
95
|
+
correctedGapCount = 0,
|
96
|
+
uncorrectedGapCount = 0,
|
97
|
+
uncorrectedGaps = [],
|
98
|
+
replacedCount = 0,
|
99
|
+
addedCount = 0,
|
100
|
+
deletedCount = 0,
|
101
|
+
onMetricClick
|
102
|
+
}: CorrectionMetricsProps) {
|
103
|
+
const formatPositionLabel = (position: number, length: number) => {
|
104
|
+
if (length === 1) {
|
105
|
+
return `Position ${position}`;
|
106
|
+
}
|
107
|
+
return `Positions ${position}-${position + length - 1}`;
|
108
|
+
};
|
109
|
+
|
110
|
+
return (
|
111
|
+
<Grid container spacing={2}>
|
112
|
+
<Grid item xs={12} sm={6} md={4}>
|
113
|
+
<Metric
|
114
|
+
color={COLORS.anchor}
|
115
|
+
label="Anchor Sequences"
|
116
|
+
value={anchorCount ?? '-'}
|
117
|
+
description="Matched sections between transcription and reference"
|
118
|
+
details={[
|
119
|
+
{ label: "Multi-source Matches", value: multiSourceAnchors },
|
120
|
+
{ label: "Spotify Only", value: singleSourceMatches.spotify },
|
121
|
+
{ label: "Genius Only", value: singleSourceMatches.genius },
|
122
|
+
]}
|
123
|
+
onClick={onMetricClick?.anchor}
|
124
|
+
/>
|
125
|
+
</Grid>
|
126
|
+
<Grid item xs={12} sm={6} md={4}>
|
127
|
+
<Metric
|
128
|
+
color={COLORS.corrected}
|
129
|
+
label="Corrected Gaps"
|
130
|
+
value={correctedGapCount ?? '-'}
|
131
|
+
description="Successfully corrected sections"
|
132
|
+
details={[
|
133
|
+
{ label: "Words Replaced", value: replacedCount },
|
134
|
+
{ label: "Words Added", value: addedCount },
|
135
|
+
{ label: "Words Deleted", value: deletedCount },
|
136
|
+
]}
|
137
|
+
onClick={onMetricClick?.corrected}
|
138
|
+
/>
|
139
|
+
</Grid>
|
140
|
+
<Grid item xs={12} sm={6} md={4}>
|
141
|
+
<Metric
|
142
|
+
color={COLORS.uncorrectedGap}
|
143
|
+
label="Uncorrected Gaps"
|
144
|
+
value={uncorrectedGapCount}
|
145
|
+
description="Sections that may need manual review"
|
146
|
+
details={uncorrectedGaps.map(gap => ({
|
147
|
+
label: formatPositionLabel(gap.position, gap.length),
|
148
|
+
value: gap.length
|
149
|
+
}))}
|
150
|
+
onClick={onMetricClick?.uncorrected}
|
151
|
+
/>
|
152
|
+
</Grid>
|
153
|
+
</Grid>
|
154
|
+
)
|
155
|
+
}
|