Skip to content

feat: implemented transcript editor#96

Closed
pganesh-apphelix wants to merge 4 commits into
release-ulmofrom
feat/TNL2-515
Closed

feat: implemented transcript editor#96
pganesh-apphelix wants to merge 4 commits into
release-ulmofrom
feat/TNL2-515

Conversation

@pganesh-apphelix
Copy link
Copy Markdown

No description provided.

Copilot AI review requested due to automatic review settings May 5, 2026 13:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an in-platform transcript editing experience for videos, refactors the video info modal to show transcript management alongside file details, and tightens transcript upload validation in both the videos page and the legacy video settings modal.

Changes:

  • Adds a new transcript editor modal with cue editing, preview syncing, validation, and autosave.
  • Reworks the video info flow so transcript management lives in the info modal sidebar/content instead of the old tabbed sidebar component.
  • Adds SRT validation and searchable language pickers for transcript upload/replace flows in both video-management surfaces.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 20 comments.

Show a summary per file
File Description
src/files-and-videos/videos-page/transcript-editor/TranscriptEditorModal.jsx New transcript editor modal, loading, autosave, cue editing, and video preview sync.
src/files-and-videos/videos-page/transcript-editor/TranscriptEditor.scss Styling for the transcript editor modal and cue UI.
src/files-and-videos/videos-page/transcript-editor/TranscriptCueBlock.jsx Editable cue block with text, timestamps, seek, delete, and insert actions.
src/files-and-videos/videos-page/transcript-editor/srtUtils.js New SRT parsing, serialization, validation, and file checks.
src/files-and-videos/videos-page/transcript-editor/messages.js i18n strings for the transcript editor.
src/files-and-videos/videos-page/transcript-editor/index.js Export for the transcript editor modal.
src/files-and-videos/videos-page/info-sidebar/VideoInfoModalSidebar.jsx Removes the old tabbed video info sidebar.
src/files-and-videos/videos-page/info-sidebar/TranscriptTab.jsx Refactors transcript management UI, adds form mode, editor modal launch, and toast handling.
src/files-and-videos/videos-page/info-sidebar/TranscriptForm.jsx New form for adding a transcript with validation and upload states.
src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.scss Expanded sidebar/transcript form/menu styling.
src/files-and-videos/videos-page/info-sidebar/transcript-item/TranscriptMenu.jsx Switches transcript actions to a dropdown and adds Edit.
src/files-and-videos/videos-page/info-sidebar/transcript-item/Transcript.jsx Updates transcript row UI, confirmation modal, and upload validation.
src/files-and-videos/videos-page/info-sidebar/transcript-item/messages.js Adds transcript edit menu label.
src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.scss Styling for searchable language selection.
src/files-and-videos/videos-page/info-sidebar/transcript-item/LanguageSelect.jsx Adds searchable language picker UI for transcript selection.
src/files-and-videos/videos-page/info-sidebar/messages.js New sidebar/form/toast copy and updated transcript title text.
src/files-and-videos/videos-page/info-sidebar/InfoTab.jsx Extends info tab content to optionally show usage metrics.
src/files-and-videos/videos-page/info-sidebar/index.js Changes default export to TranscriptTab.
src/files-and-videos/videos-page/index.js Removes VideoInfoModalSidebar from public exports.
src/files-and-videos/videos-page/data/slice.js Deduplicates stored video IDs in reducers.
src/files-and-videos/videos-page/CourseVideosTable.tsx Replaces old sidebar injection with custom info-modal content for details/transcripts.
src/files-and-videos/index.scss Wires transcript editor styles into the files-and-videos bundle.
src/files-and-videos/generic/table-components/table-custom-columns/TranscriptColumn.jsx Makes transcript count clickable when file info can be opened.
src/files-and-videos/generic/table-components/table-custom-columns/MoreInfoColumn.jsx Closes menu after actions and updates video info label.
src/files-and-videos/generic/messages.js Adds video-specific info menu text.
src/files-and-videos/generic/InfoModal.jsx Refactors modal content injection from sidebar callback to render-content model.
src/files-and-videos/generic/FileTable.jsx Adds context-driven cell wrappers, unique-file filtering, and new info modal content API.
src/files-and-videos/generic/FileMenu.jsx Updates video info label in file card menu.
src/files-and-videos/files-page/CourseFilesTable.tsx Adapts files page to the new info-modal content API.
src/editors/containers/VideoEditor/components/VideoSettingsModal/index.scss Adds searchable language-menu styling for legacy transcript widget.
src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.tsx Updates test setup for new transcript validation callback signature.
src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.jsx Adds SRT validation before replacing a transcript.
src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/Transcript.jsx Adds transcript upload validation error alerts in the legacy widget.
src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/messages.js Adds new validation error messages.
src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/LanguageSelector.jsx Adds searchable language selection and SRT validation before upload.
src/data/api.ts Adds the transcriptEditor waffle flag default.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +63 to +70
useEffect(() => {
if (!enabled) { return undefined; }
if (content === lastSavedRef.current) { return undefined; }
if (timerRef.current) { clearTimeout(timerRef.current); }
timerRef.current = setTimeout(() => { flush(content); }, debounceMs);
return () => {
if (timerRef.current) { clearTimeout(timerRef.current); }
};
Comment on lines +37 to +38
onEmptyFail: () => setLocalError('empty'),
onSizeFail: onFileTooLarge,
Comment on lines +44 to +48
validateSrtFile(file, {
onEmptyFail: onEmptyFile,
onSizeFail,
onInvalidFail: onInvalidFile,
onValid: (f) => handleTranscript({ file: f, language, newLanguage }, 'upload'),
Comment on lines +77 to +83
<input
type="text"
className="language-select__search-input"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
Comment on lines +134 to +140
<input
type="text"
className="language-selector-search__input"
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
Comment on lines +50 to +58
} catch (err) {
if (inFlightRef.current === requestId) {
setStatus('error');
setError(
err?.response?.data?.detail
|| err?.response?.data?.error
|| err?.message
|| 'Save failed',
);
export {
TranscriptSettings,
UploadModal,
VideoThumbnail,
Comment on lines +1 to +3
import TranscriptTab from './TranscriptTab';

export default VideoInfoModalSidebar;
export default TranscriptTab;
Comment on lines +80 to +90
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
/>
</div>
</div>
<Menu className="language-select__list">
<div>
{filteredEntries.length === 0 && (
<div className="language-select__no-results">No results</div>
Comment on lines +137 to +146
placeholder="Search..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
/>
</div>
</div>
<div className="language-selector-list">
{filteredLanguages.length === 0 && (
<div className="language-selector-no-results">No results</div>
Copilot AI review requested due to automatic review settings May 8, 2026 09:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

src/editors/containers/VideoEditor/components/VideoSettingsModal/components/TranscriptWidget/TranscriptActionMenu.test.tsx:62

  • This test now calls replaceFileCallback with a plain object (mockEvent) that doesn't have size/File semantics. Since replaceFileCallback now delegates to validateSrtFile (which checks file.size and uses FileReader), thunkActions.video.replaceTranscript will never be called and the assertions will fail. Update the test to use a real File (or mock validateSrtFile/FileReader to invoke onValid) and assert the async dispatch accordingly.
      test('it dispatches the correct thunk', () => {
        const cb = componentModule.hooks.replaceFileCallback({
          dispatch: mockDispatch,
          language: lang1Code,
          onEmptyFail: jest.fn(),
          onSizeFail: jest.fn(),
          onInvalidFail: jest.fn(),
        });
        cb(mockEvent);
        expect(thunkActions.video.replaceTranscript).toHaveBeenCalledWith(result);
        expect(mockDispatch).toHaveBeenCalledWith({ replaceTranscript: result });
      });

Comment on lines +62 to +72
useEffect(() => {
if (
prevTranscriptStatusRef.current === RequestStatus.IN_PROGRESS
&& transcriptStatus === RequestStatus.SUCCESSFUL
) {
if (lastActionRef.current === 'delete') {
showToast(intl.formatMessage(messages.transcriptDeletedToast));
} else {
showToast(intl.formatMessage(messages.transcriptUploadedToast));
setIsAddingNew(false);
}
Comment on lines 90 to 95
case 'delete':
if (isEmpty(language)) {
const updatedSelection = previousSelection;
updatedSelection.shift();
setPreviousSelection(updatedSelection);
} else {
}
if (file) {
return (
<Stack direction="horizontal" className="new-transcript-form__file-row d-flex align-items-centergap-2 py-2 px-1 small">
Comment on lines +357 to +361
{loadStatus === 'loading' && (
<Stack direction="horizontal" className="align-items-center text-muted">
<Spinner animation="border" size="sm" screenReaderText="" className="mr-2" />
{intl.formatMessage(messages.loading)}
</Stack>
Comment on lines +54 to +59
setError(
err?.response?.data?.detail
|| err?.response?.data?.error
|| err?.message
|| 'Save failed',
);
Comment on lines +146 to +153
const { status: saveStatus, error: saveError } = useTranscriptAutoSave({
videoId,
language,
content: serialized,
apiUrl: transcriptUploadHandlerUrl,
filename: `${videoFilename || videoId}-${language}.srt`,
enabled: isOpen && hasEdited && loadStatus === 'loaded' && !hasValidationErrors,
});
Comment on lines +45 to +51
const input = fileInput({
onAddFile: module.hooks.replaceFileCallback({
language,
dispatch: useDispatch(),
onEmptyFail,
onSizeFail,
onInvalidFail,
Comment on lines +7 to +66
export function parseSrt(srt) {
if (!srt || typeof srt !== 'string') {
return [];
}
const blocks = srt.replace(/\r\n/g, '\n').trim().split(/\n\s*\n/);
const cues = [];
blocks.forEach((block, i) => {
const lines = block.split('\n').filter((l) => l !== undefined);
if (lines.length < 2) {
return;
}
let cursor = 0;
let index = i + 1;
if (!TIMESTAMP_RE.test(lines[0])) {
const parsed = parseInt(lines[0].trim(), 10);
if (!Number.isNaN(parsed)) {
index = parsed;
}
cursor = 1;
}
const tsLine = lines[cursor];
const m = tsLine && tsLine.match(TIMESTAMP_RE);
if (!m) {
return;
}
const [, h1, m1, s1, ms1, h2, m2, s2, ms2] = m;
const startMs = (((+h1 * 60) + +m1) * 60 + +s1) * 1000 + +ms1;
const endMs = (((+h2 * 60) + +m2) * 60 + +s2) * 1000 + +ms2;
const text = lines.slice(cursor + 1).join('\n');
cues.push({
index,
startMs,
endMs,
startText: `${h1}:${m1}:${s1},${ms1}`,
endText: `${h2}:${m2}:${s2},${ms2}`,
text,
});
});
return cues;
}

export function msToSrtTime(ms) {
const totalSec = Math.floor((ms || 0) / 1000);
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
const millis = Math.max(0, (ms || 0) - totalSec * 1000);
return `${pad2(h)}:${pad2(m)}:${pad2(s)},${pad3(millis)}`;
}

export function serializeSrt(cues) {
return cues
.map((cue, i) => {
const start = cue.startText || msToSrtTime(cue.startMs);
const end = cue.endText || msToSrtTime(cue.endMs);
return `${i + 1}\n${start} --> ${end}\n${cue.text}`;
})
.join('\n\n')
.concat('\n');
}
Copilot AI review requested due to automatic review settings May 12, 2026 07:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 59 out of 59 changed files in this pull request and generated 6 comments.

Comment on lines +281 to +284
// Reset when a fresh transcript is loaded.
useEffect(() => {
setRevealCount(INITIAL_REVEAL);
}, [cues]);
Comment on lines +345 to +351
const start = () => {
if (cancelled) { return; }
let track = Array.from(video.textTracks)
.find((t) => t.kind === 'subtitles' && t.language === language);
if (!track) {
track = video.addTextTrack('subtitles', languageName || language, language);
textTrackRef.current = track;
Comment on lines +23 to +31
<Toast
className={classNames(
`processing-notification processing-notification--${variant}`,
{ 'processing-notification-hide-close-button': !close },
)}
show={isShow}
aria-hidden={isShow}
action={action && { ...action }}
onClose={close || (() => {})}
Comment on lines 54 to 56
duration: PropTypes.number.isRequired,
dateAdded: PropTypes.string.isRequired,
fileSize: PropTypes.number.isRequired,
Comment on lines +118 to +122
aria-label={intl.formatMessage(messages.cueAriaLabel, { timestamp })}
aria-invalid={errors && errors.length > 0 ? 'true' : 'false'}
rows={1}
controlClassName={`transcript-cue-block__textarea${errors && errors.length ? ' transcript-cue-block__textarea--invalid' : ''}`}
spellCheck
Comment on lines +34 to +41
languageSearchLabel: {
id: 'course-authoriong.video-uploads.file-info.transcripts.languageSearchLabel',
defaultMessage: 'Search languages',
description: 'Accessible label for the transcript language search input.',
},
languageSearchPlaceholder: {
id: 'course-authoriong.video-uploads.file-info.transcripts.languageSearchPlaceholder',
defaultMessage: 'Search languages',
@abhalsod-sonata
Copy link
Copy Markdown

Close PR because as a all change include in #97

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants