Conversation
- Complete UI overhaul of MediaPreviewView, MediaProcessingView, and MediaSelectionView - Add motion-v animations and enhanced visual design with gradients and shadows - Implement step-by-step media processing workflow with progress indicators - Add AGENTS.md coding guidelines document - Fix FFmpeg instance cleanup and improve error handling - Update types for media step configuration and processing
Extract MediaProgressView component and useTaskListener composable for better separation of concerns. Replace command bus pattern with direct task polling, simplify error handling, and move layout structure to default layout.
… layout - Add motion-v animations for page transitions, buttons, and sections - Teleport transcription info to layout header (left side) as popover - Teleport export toolbar to layout header (right side) - Add loading state with animated spinner - Enhance summary section with amber gradient styling - Improve mode toggle with sliding indicator animation - Add i18n translations for nameLabel and noMedia - Make layout more mobile-friendly with responsive design
In the export toolbar and a new button in the summary view.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughThis pull request refactors the application architecture from Pinia store-based state management to composition API with Dexie.js IndexedDB persistence. It introduces a new media workflow (selection → preview → processing), adds comprehensive transcription editing and export features, restructures API endpoints using a builder pattern, and corrects command type naming inconsistencies throughout the codebase. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Client as Web Client
participant MediaWF as Media Workflow
participant ProcessAPI as Processing API
participant TaskService as Task Service
participant DB as IndexedDB
User->>Client: Select/Record Media
Client->>MediaWF: MediaSelectionView
MediaWF-->>Client: Media File
User->>Client: Configure Settings
Client->>MediaWF: MediaPreviewView
MediaWF-->>Client: Config (language, speakers)
Client->>ProcessAPI: POST /api/transcribe/submit
ProcessAPI-->>Client: Task Created {task_id}
Client->>DB: Store Task Status
Client->>ProcessAPI: Poll /api/transcribe/{task_id}/status
ProcessAPI-->>Client: Progress Updates
Client->>MediaWF: Display MediaProgressView
ProcessAPI-->>Client: Status: COMPLETED
Client->>ProcessAPI: GET /api/transcribe/{task_id}
ProcessAPI-->>Client: Transcription Result
Client->>DB: Store Transcription
Client->>DB: Update Task Status
Client->>Client: Navigate to Transcription Page
User->>Client: Edit/View Transcription
Client->>MediaWF: TranscriptionEditView
MediaWF->>DB: Load Segments, Media
MediaWF-->>Client: Rendered Transcription
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request delivers a major update focused on enhancing the user interface and introducing several new core features to improve the transcription and editing experience. The changes aim to provide a more visually appealing, interactive, and functional application, while also modernizing the underlying data management and development practices. The new features empower users with more control over their transcriptions, from detailed editing to advanced reporting and export options. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This is an impressive and extensive pull request that significantly refactors and enhances the application's UI and underlying architecture. The migration from Pinia stores and a manual IndexedDB wrapper to Dexie-backed composables is a major improvement for state management and data persistence. The new component-based structure, the polished multi-step upload process, and the addition of features like DOCX export and speaker statistics greatly improve the user experience and maintainability of the codebase. The code is well-structured and the new abstractions are thoughtfully designed. I've found a few issues, including one critical bug in a new component and some minor areas for improvement, which I've detailed in the comments.
There was a problem hiding this comment.
Actionable comments posted: 4
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/components/MediaEditor.vue (1)
27-51:⚠️ Potential issue | 🟠 MajorReinitialize media metadata when
transcriptionchanges, not only on mount.With prop-driven flow, mount-only initialization can leave stale
audioFile,duration, andtimeRangewhen the prop updates in the same component instance.Suggested patch
-onMounted(() => { - duration.value = 0; - const currentTranscription = props.transcription; +function loadFromTranscription(currentTranscription: StoredTranscription) { + duration.value = 0; if (!currentTranscription?.mediaFile) { + audioFile.value = undefined; + timeRange.value = [0, 0]; return; } @@ - currentTime.value = 0; -}); + currentTime.value = 0; +} + +onMounted(() => loadFromTranscription(props.transcription)); + +watch( + () => props.transcription, + (next) => loadFromTranscription(next), +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaEditor.vue` around lines 27 - 51, The initialization of audioFile/duration/timeRange is only done in onMounted, so updates to props.transcription leave stale state; extract the media setup logic into a reusable function (e.g., initializeMedia or updateMediaMetadata) that sets audioFile.value, creates an object URL, creates an Audio to read onloadedmetadata, sets duration.value, timeRange.value and currentTime.value, and revokes/cleans previous object URLs and audio event handlers; call that function from onMounted and also add a watch on props.transcription to re-run it whenever the prop changes, ensuring you handle null/undefined mediaFile branches and revoke any prior URL to avoid leaks.
🟠 Major comments (32)
app/components/KeyboardShortcutsHint.vue-10-13 (1)
10-13:⚠️ Potential issue | 🟠 Majori18n breakage: string manipulation with hardcoded English substrings.
The
.replace('Press ', '').replace(' to play/pause the video or audio', '')manipulates the translated string using hardcoded English phrases. This will fail for non-English locales (e.g., German) where the translated text doesn't contain these exact substrings.Create a dedicated i18n key for the shortcut description instead of manipulating an existing translation.
Proposed fix
Add a new i18n key (e.g.,
help.shortcuts.playPause) with the short description, then use it directly:{ keys: ['Space'], - description: t('help.mediaControls.spacebar', { space: 'Space' }).replace('Press ', '').replace(' to play/pause the video or audio', ''), + description: t('help.shortcuts.playPause'), },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/KeyboardShortcutsHint.vue` around lines 10 - 13, Replace the brittle string-manipulation approach in KeyboardShortcutsHint.vue (the object with keys: ['Space'] and description: t('help.mediaControls.spacebar').replace(...)) by adding a new, concise i18n key (e.g., help.shortcuts.playPause) in your locale files with the short description, then use t('help.shortcuts.playPause') for the description field; remove the .replace(...) calls and update all locale JSON/YAML files to include the new key so translations remain correct across languages.tests/unit/services/colorMapService.test.ts-67-102 (1)
67-102:⚠️ Potential issue | 🟠 MajorAdd a runtime instance assertion for
grayscale.At Line 67 onward,
grayscaleis the only map block withouttoBeInstanceOf(RGBColor). That misses an API-contract regression currently visible inapp/services/colorMapService.ts(Line 56-61), wheregrayscalereturns a plain object.✅ Suggested test addition
describe("grayscale", () => { @@ it("should clamp values above 1", () => { const result = grayscale(1.5); expect(result).toEqual(grayscale(1)); }); + + it("should return RGBColor instance", () => { + expect(grayscale(0.5)).toBeInstanceOf(RGBColor); + }); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/services/colorMapService.test.ts` around lines 67 - 102, Add a runtime instance assertion to the grayscale tests by calling expect(result).toBeInstanceOf(RGBColor) (use the RGBColor class) after obtaining result from grayscale; if that test fails, modify the grayscale implementation so it returns an actual RGBColor instance (e.g., new RGBColor(...)) instead of a plain object—update the function named grayscale and ensure it constructs/returns RGBColor instances.app/components/UploadMediaView.client.vue-238-242 (1)
238-242:⚠️ Potential issue | 🟠 MajorWire
UFileUploadto the same file-processing path.
UFileUploadcurrently has no event bindings and is non-functional. Users can interact with it but no file processing occurs. TheloadAudiohandler is bound only to the legacyUInputelement. Add eitherv-model="selectedFile"with a watcher or@update:modelValue="handleFileUpload"to theUFileUploadcomponent, or bind@change="loadAudio"directly (though note that@changeonUFileUploademits a nativeEvent, similar toUInput).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/UploadMediaView.client.vue` around lines 238 - 242, UFileUpload is not wired to the existing file processing path so uploaded files are ignored; hook it into the same handler as the legacy UInput by adding one of the following to the UFileUpload tag: v-model="selectedFile" with a watcher that calls loadAudio, or `@update`:modelValue="handleFileUpload" that forwards the file to loadAudio, or simply `@change`="loadAudio" to reuse the existing loadAudio handler; ensure the event payload matches loadAudio's expectations (extract event.target.files[0] if necessary) and use the same ref/selectedFile variable used by UInput (e.g. fileInputRef/selectedFile) so both inputs share the same processing flow.tests/unit/services/indexDbService.test.ts-13-29 (1)
13-29:⚠️ Potential issue | 🟠 MajorThese constant tests are tautologies and won’t catch real regressions.
On Line 14, Line 19, Line 20, and Line 27, constants are declared inside the test and asserted against the same hardcoded values. This always passes even if the real DB config changes. Please assert against values imported/read from the actual IndexedDB/Dexie module instead.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/services/indexDbService.test.ts` around lines 13 - 29, The tests currently declare and assert local constants (DB_NAME, TRANSCIPTION_STORE_NAME, TASK_STORE_NAME, DB_VERSION) which makes them tautological; instead remove those local declarations and import the real exported constants or config from the module under test (e.g., indexDbService or the module that exports DB_NAME, TRANSCIPTION_STORE_NAME, TASK_STORE_NAME, DB_VERSION) and assert that the imported values equal the expected literal strings/numbers; update the three "it" blocks to reference the imported symbols (DB_NAME, TRANSCIPTION_STORE_NAME, TASK_STORE_NAME, DB_VERSION) rather than redefining them so the tests will fail if the real config changes.app/components/transcription/ProcessingTasksTable.vue-52-52 (1)
52-52:⚠️ Potential issue | 🟠 MajorRemove the root
v-ifso empty/error states can render.Line [52] hides the entire section when there are no tasks, so users won’t see the table empty-state, refresh button, or error alert.
Proposed fix
- <div v-if="props.tasks.length > 0" class="space-y-6 mb-8"> + <div class="space-y-6 mb-8">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/transcription/ProcessingTasksTable.vue` at line 52, The root template div in ProcessingTasksTable.vue currently uses v-if="props.tasks.length > 0" which hides the entire component (including empty-state, refresh button, and error alert); remove that root v-if and instead apply conditional rendering only where needed — e.g., keep the table and its header always rendered and move v-if/v-else checks to the tbody or rows (use v-if="props.tasks.length === 0" to show the empty-state/refresh button and v-for over props.tasks to render rows), and ensure any error alert uses its own condition so errors display even when tasks is empty.app/stores/db.ts-13-13 (1)
13-13:⚠️ Potential issue | 🟠 MajorFix typo in transcription index key (
audioFiledId).
audioFiledIdappears misspelled and can break index-based queries if the stored property isaudioFileId.Proposed fix
db.version(3).stores({ tasks: "id, status, mediaFile, mediaFileName, createdAt", transcriptions: - "id, segments, name, createdAt, updatedAt, audioFiledId, mediaFile, mediaFileName, summary", + "id, segments, name, createdAt, updatedAt, audioFileId, mediaFile, mediaFileName, summary", });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/stores/db.ts` at line 13, The transcription index key string contains a typo: replace "audioFiledId" with the correct "audioFileId" in the index definition (the quoted fields list containing "id, segments, name, createdAt, updatedAt, audioFiledId, mediaFile, mediaFileName, summary") so index-based queries use the proper property name; update that string wherever it appears in app/stores/db.ts (e.g., the transcription index/fields list) to match the stored property.package.json-44-44 (1)
44-44:⚠️ Potential issue | 🟠 MajorDowngrade
vue-routerto 4.x—Nuxt 4.3.1 does not support Vue Router 5.0.2.Nuxt 4.3.1 is built and tested against Vue Router 4.x only (pinned to
^4.6.4in its peer dependencies). Vue Router 5.0.2 is a transition release designed for direct Vue Router users, but Nuxt has not documented support for it. Change line 44 to"vue-router": "^4.6.4"or allow Nuxt to resolve its compatible router version.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 44, The package.json currently pins "vue-router" to 5.0.2 which is incompatible with Nuxt 4.3.1; update the dependency entry for "vue-router" (the package.json "vue-router" field) to a Nuxt-compatible version such as "^4.6.4" or remove the explicit pin so Nuxt can resolve its supported 4.x version, then run your lockfile update (npm/yarn/pnpm) to regenerate the lockfile and verify the install.app/components/transcription/TranscriptionTable.vue-81-81 (1)
81-81:⚠️ Potential issue | 🟠 MajorUse an absolute route to prevent nested path duplication.
Line 81 uses a relative
to, which resolves relative to the current route. When this component renders on/transcription, the relative pathtranscription/${id}resolves to/transcription/transcription/:idinstead of the intended/transcription/:id.Fix
- <ULink :to="`transcription/${row.original.id}`"> + <ULink :to="`/transcription/${row.original.id}`">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/transcription/TranscriptionTable.vue` at line 81, The ULink usage in TranscriptionTable.vue uses a relative route string (`transcription/${row.original.id}`) which causes nested paths; update the :to prop on the ULink component to use an absolute path (e.g. start with a leading slash or use a named route) so it resolves to /transcription/:id instead of nesting under the current route; locate the ULink element in the TranscriptionTable.vue template (the line with <ULink :to="..."> referencing row.original.id) and change the route string to an absolute route or switch to a route object with name and params.app/composables/useTranscriptionSummary.ts-70-79 (1)
70-79:⚠️ Potential issue | 🟠 MajorDo not clear existing summary before successful regeneration.
Current flow clears
transcription.summaryfirst, then calls the API. If the request fails, the previous summary is lost from current state.Suggested fix
-const isRegeneration = !!transcription.summary; +const previousSummary = transcription.summary; +const isRegeneration = !!previousSummary; ... -// If regenerating, clear the existing summary immediately -if (isRegeneration) { - transcription.summary = undefined; -} +// Keep existing summary until replacement is successfully generated ... } catch (error) { + if (isRegeneration) { + transcription.summary = previousSummary; + } logger.error(error, "Failed to generate summary:"); throw error; }Also applies to: 109-112
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/useTranscriptionSummary.ts` around lines 70 - 79, The code currently clears transcription.summary immediately when isRegeneration is true, which loses the previous summary if the API call fails; instead, preserve the existing summary until the regeneration request succeeds: do not set transcription.summary = undefined before calling the API in useTranscriptionSummary; call the API while leaving transcription.summary intact (you can set isSummaryGenerating or an isRegenerating flag to drive UI), and only replace transcription.summary with the new summary on successful response (and leave the old value unchanged on error). Apply the same change for the second occurrence (the block around the code referenced at lines 109-112).app/components/TranscriptionInfoView.vue-18-26 (1)
18-26:⚠️ Potential issue | 🟠 MajorAvoid creating object URLs inside computed without cleanup.
Line 20 creates a new object URL via computed state, but old URLs are never revoked. This can leak memory when media/transcription changes.
Suggested fix
-const mediaUrl = computed(() => { - if (props.transcription.mediaFile) { - return URL.createObjectURL( - props.transcription.mediaFile, - ); - } - - return undefined; -}); +const mediaUrl = ref<string>(); + +watch( + () => props.transcription.mediaFile, + (file, _, onCleanup) => { + if (mediaUrl.value) URL.revokeObjectURL(mediaUrl.value); + mediaUrl.value = file ? URL.createObjectURL(file) : undefined; + onCleanup(() => { + if (mediaUrl.value) URL.revokeObjectURL(mediaUrl.value); + }); + }, + { immediate: true }, +);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/TranscriptionInfoView.vue` around lines 18 - 26, The computed mediaUrl creates Object URLs but never revokes old ones; update the setup so that when props.transcription.mediaFile changes or the component unmounts you revoke the previous URL. Specifically, keep a local let currentObjectUrl variable alongside the mediaUrl computed (referencing mediaUrl and props.transcription.mediaFile), generate the new URL with URL.createObjectURL, revoke the previous URL with URL.revokeObjectURL(currentObjectUrl) before assigning the new one, and also revoke currentObjectUrl in an onBeforeUnmount hook to avoid leaks. Ensure mediaUrl returns the currentObjectUrl (or undefined) so consumers are unchanged.tests/composables/useTaskStatus.test.ts-11-54 (1)
11-54:⚠️ Potential issue | 🟠 MajorTests are validating duplicated logic, not the production composable.
Lines 11-54 re-declare the same helpers instead of calling
useTaskStatus(). This makes the suite non-protective against regressions inapp/composables/useTaskStatus.ts.Suggested fix
-import { TaskStatusEnum, type TaskStatus } from "../../../app/types/task"; - -function getStatusDisplay(status: string): string { - ... -} - -function getStatusColor(status: string): string { - ... -} - -function computeTaskProgress(taskStatus: { status: string; progress: number | null }): number { - ... -} +import { TaskStatusEnum } from "../../../app/types/task"; +import { useTaskStatus } from "../../../app/composables/useTaskStatus";describe("useTaskStatus logic", () => { + const { getStatusDisplay, getStatusColor, computeTaskProgress } = useTaskStatus(); + beforeEach(() => { vi.clearAllMocks(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/composables/useTaskStatus.test.ts` around lines 11 - 54, The tests duplicate helper implementations (getStatusDisplay, getStatusColor, computeTaskProgress) instead of exercising the real composable; replace these local function copies by importing and calling the production composable useTaskStatus() from app/composables/useTaskStatus.ts in the test, and update assertions to call the returned helpers (e.g., const { getStatusDisplay, getStatusColor, computeTaskProgress } = useTaskStatus()) so the test validates the actual logic and prevents regressions in the production code.app/components/SpeakerStatisticsView.vue-37-62 (1)
37-62:⚠️ Potential issue | 🟠 MajorHandle media URL lifecycle and transcription changes reactively.
Line 45 creates an object URL inside
onMounted, but cleanup only happens in async callbacks. If the component unmounts early (ortranscription.mediaFilechanges), stale URLs/listeners can leak andmediaDurationcan stay stale.Suggested fix
- onMounted(() => { - if (!props.transcription.mediaFile) { - const lastSegment = props.transcription.segments[props.transcription.segments.length - 1]; - mediaDuration.value = lastSegment?.end ?? 0; - isLoadingDuration.value = false; - return; - } - - const audioSrc = URL.createObjectURL(props.transcription.mediaFile); - const audio = new Audio(); - audio.src = audioSrc; - - audio.onloadedmetadata = () => { - mediaDuration.value = audio.duration; - isLoadingDuration.value = false; - URL.revokeObjectURL(audioSrc); - audio.onloadedmetadata = null; - }; - - audio.onerror = () => { - const lastSegment = props.transcription.segments[props.transcription.segments.length - 1]; - mediaDuration.value = lastSegment?.end ?? 0; - isLoadingDuration.value = false; - URL.revokeObjectURL(audioSrc); - }; - }); +let activeAudioSrc: string | undefined; +let activeAudio: HTMLAudioElement | undefined; + +function fallbackDuration() { + const lastSegment = props.transcription.segments[props.transcription.segments.length - 1]; + mediaDuration.value = lastSegment?.end ?? 0; + isLoadingDuration.value = false; +} + +watch( + () => [props.transcription.mediaFile, props.transcription.segments] as const, + () => { + if (activeAudioSrc) URL.revokeObjectURL(activeAudioSrc); + if (activeAudio) { + activeAudio.onloadedmetadata = null; + activeAudio.onerror = null; + } + + isLoadingDuration.value = true; + if (!props.transcription.mediaFile) return fallbackDuration(); + + activeAudioSrc = URL.createObjectURL(props.transcription.mediaFile); + activeAudio = new Audio(activeAudioSrc); + activeAudio.onloadedmetadata = () => { + mediaDuration.value = activeAudio?.duration ?? 0; + isLoadingDuration.value = false; + if (activeAudioSrc) URL.revokeObjectURL(activeAudioSrc); + activeAudioSrc = undefined; + }; + activeAudio.onerror = fallbackDuration; + }, + { immediate: true }, +); + +onUnmounted(() => { + if (activeAudioSrc) URL.revokeObjectURL(activeAudioSrc); + if (activeAudio) { + activeAudio.onloadedmetadata = null; + activeAudio.onerror = null; + } +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/SpeakerStatisticsView.vue` around lines 37 - 62, The onMounted block in SpeakerStatisticsView.vue creates an object URL and audio listeners but doesn't clean them up if the component unmounts or transcription.mediaFile changes, causing leaks and stale mediaDuration; update the logic to (1) store the created audio instance and audioSrc, (2) register an onUnmounted cleanup that revokes the object URL, removes listeners and pauses/nulls the audio, and (3) add a watch on props.transcription.mediaFile (or the entire props.transcription) to revoke any previous URL, remove prior listeners, reset isLoadingDuration/mediaDuration, and recreate the audio when the file changes; target the audio variable, audioSrc constant, the audio.onloadedmetadata/audio.onerror handlers, and the onMounted/onUnmounted lifecycle usage.app/components/MediaProcessingView.vue-37-45 (1)
37-45:⚠️ Potential issue | 🟠 MajorAdd a top-level try/catch for the media-processing pipeline.
processMedia()is started fromonMountedwithout an error boundary, so preprocess/upload failures can become unhandled promise rejections.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaProcessingView.vue` around lines 37 - 45, processMedia() lacks a top-level error boundary so failures in preprocessMedia, uploadFile, or waitForTask can become unhandled rejections; wrap the entire async body of processMedia() in a try/catch (called from onMounted) and handle errors there—log or assign to a component error state and ensure any cleanup or UI updates (e.g., setting a loading flag) occur in the catch/finally so failures from preprocessMedia, uploadFile, or waitForTask are properly handled and do not propagate as unhandled promise rejections.app/components/media/VideoView.vue-133-136 (1)
133-136:⚠️ Potential issue | 🟠 MajorUse actual media MIME type in
<source>.
type="video/mp4"is too narrow and can fail for valid non-MP4 video blobs. BindmediaFile.typeinstead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/VideoView.vue` around lines 133 - 136, The <source> element currently hardcodes type="video/mp4" which can mislabel other valid video blobs; update the VideoView.vue template so the source type is bound to the actual media MIME type (use mediaFile.type) instead of the literal "video/mp4" — locate the <video> block using refs like videoElement and bindings mediaSrc/isVideoFile and change the <source> type to use the mediaFile.type property so the browser receives the correct MIME for playback.app/components/media/VideoView.vue-87-102 (1)
87-102:⚠️ Potential issue | 🟠 MajorRevoke old blob URLs and reset media state on transcription change.
The watcher creates new object URLs but never revokes the previous one. It also returns early when no
mediaFile, leaving stalemediaSrc/mediaFile/isVideoFilefrom prior transcription.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/VideoView.vue` around lines 87 - 102, The watcher on props.transcription must revoke any previous blob URL and fully reset media state when the new transcription has no mediaFile; before creating a new URL call URL.revokeObjectURL(mediaSrc.value) if mediaSrc.value is set, and when currentTranscription?.mediaFile is falsy clear mediaFile.value, mediaSrc.value (after revoking), segments.value = [], isVideoFile.value = false and isPlaying.value = false; otherwise revoke the old URL first, set mediaFile.value, set mediaSrc.value = URL.createObjectURL(...), update segments.value and isVideoFile.value with checkIfVideoFile, and set isPlaying.value = false.app/components/MediaPlaybackBar.vue-66-73 (1)
66-73:⚠️ Potential issue | 🟠 MajorReset media state when no
transcription.mediaFileexists.
loadMedia()exits early without clearingmediaFile,mediaSrc, andisVideoFile, so previous media can remain active after switching to a transcription without media.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaPlaybackBar.vue` around lines 66 - 73, In loadMedia(), when props.transcription?.mediaFile is falsy you must clear the old media state so previous media doesn't stay active: inside the early-return path (in function loadMedia) revoke any existing object URL if mediaSrc.value is set, then reset mediaFile (e.g., mediaFile.value = null), clear mediaSrc (mediaSrc.value = '' or null) and set isVideoFile to false (isVideoFile.value = false); ensure the existing URL.revokeObjectURL(mediaSrc.value) call is performed only when mediaSrc.value is truthy before clearing.app/components/TranscriptionEditView.vue-31-45 (1)
31-45:⚠️ Potential issue | 🟠 MajorPrevent stale duration updates and blob URL leaks in
initializeDuration.
initializeDuration()creates a new blob URL on every call, but revokes only inonloadedmetadata. If metadata never loads (or an older load resolves after a newer one), URLs can leak anddurationcan be set from stale media.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/TranscriptionEditView.vue` around lines 31 - 45, initializeDuration currently creates a new blob URL and Audio each call but only revokes the URL in onloadedmetadata, which can leak URLs and allow stale metadata to overwrite duration; modify initializeDuration to track and clean up any previous audio/audioSrc (e.g., keep module-level or component refs currentAudio and currentAudioSrc), revoke and null out handlers for the previous audioSrc before creating a new one, set both audio.onloadedmetadata and audio.onerror to revoke the blob URL and clear handlers, and guard the handler so it only updates duration.value if the audio instance or a unique token matches the currentAudio/currentToken to prevent stale updates from older loads. Ensure every created blob URL is revoked in all code paths and previous audio handlers are removed.app/components/MediaProcessingView.vue-94-98 (1)
94-98:⚠️ Potential issue | 🟠 MajorThrow an
Errorvalue, not theRefobject.
throw errorMessagethrows the ref container instead of an error/message payload.Suggested fix
- throw errorMessage; + throw new Error(errorMessage.value ?? "Upload failed");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaProcessingView.vue` around lines 94 - 98, The code is throwing the Ref container (errorMessage) instead of an Error; after detecting isApiError(response) and setting errorMessage.value = t(`errors.${response.errorId}`), log the response as now, but throw a real Error (e.g., new Error(errorMessage.value)) or otherwise throw the string payload (errorMessage.value) instead of the Ref itself; update the throw site that currently does "throw errorMessage" to throw the actual message derived from errorMessage.value so callers receive a proper Error/ message rather than a Ref.app/composables/useTaskListener.ts-30-37 (1)
30-37:⚠️ Potential issue | 🟠 MajorNormalize progress values to the 0–100 scale.
calculateProgress()returns1forCOMPLETED/FAILED, while the UI progress model uses percent-style values.Suggested fix
- .with(TaskStatusEnum.COMPLETED, () => 1) - .with(TaskStatusEnum.FAILED, () => 1) + .with(TaskStatusEnum.COMPLETED, () => 100) + .with(TaskStatusEnum.FAILED, () => 100)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/useTaskListener.ts` around lines 30 - 37, calculateProgress currently returns 1 for COMPLETED/FAILED and may return a fractional 0–1 progress for IN_PROGRESS, but the UI expects 0–100 percent values; update calculateProgress (function calculateProgress, TaskStatusEnum, taskStatus.progress) to always return a 0–100 integer/number: return 100 for COMPLETED and FAILED, 0 for CANCELLED, and for IN_PROGRESS derive progress = taskStatus.progress ?? 0 then if progress is between 0 and 1 treat it as a fraction and multiply by 100, otherwise use the value as-is, and finally clamp the result to the 0–100 range (e.g., via Math.max/Math.min) before returning.nuxt.config.ts-28-28 (1)
28-28:⚠️ Potential issue | 🟠 MajorParse
DUMMYas boolean, not string.
process.env.DUMMY || ""returns a string;"false"is truthy and can accidentally enable dummy mode.Suggested fix
- useDummyData: process.env.DUMMY || "", + useDummyData: String(process.env.DUMMY).toLowerCase() === "true",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@nuxt.config.ts` at line 28, The useDummyData assignment currently sets useDummyData to a string (process.env.DUMMY || ""), which treats "false" as truthy; update the assignment for the useDummyData config entry to produce a boolean instead (e.g., parse process.env.DUMMY explicitly by comparing to "true" or using a safe JSON parse and providing a default false) so that useDummyData is true only when DUMMY is explicitly set to a truthy value; modify the expression that assigns useDummyData in nuxt config (the useDummyData key) accordingly.app/composables/useTasks.ts-64-70 (1)
64-70:⚠️ Potential issue | 🟠 Major
cleanupFailedAndCanceledTaskscurrently skipsCANCELLED.The filter deletes
FAILEDandCOMPLETED, but notCANCELLED, which contradicts the function contract.Suggested fix
- t.status.status === TaskStatusEnum.FAILED || - t.status.status === TaskStatusEnum.COMPLETED, + t.status.status === TaskStatusEnum.FAILED || + t.status.status === TaskStatusEnum.CANCELLED,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/useTasks.ts` around lines 64 - 70, The cleanupFailedAndCanceledTasks function currently filters tasks by TaskStatusEnum.FAILED and TaskStatusEnum.COMPLETED but should target FAILED and CANCELLED per its name; update the filter in cleanupFailedAndCanceledTasks (the db.tasks.filter call) to use TaskStatusEnum.CANCELLED instead of TaskStatusEnum.COMPLETED so the function removes FAILED and CANCELLED tasks as intended.server/utils/dummyData.ts-128-133 (1)
128-133:⚠️ Potential issue | 🟠 MajorURL parsing may extract wrong segment for task_id.
The URL pattern used in
status.get.tsis/task/[r:task_id]/status, meaning the task_id is not the last segment—"status"is. Callingoptions.url.split("/").pop()will return"status"instead of the actual task ID.🐛 Proposed fix
export function dummyTaskStatusFetcher(options: { url: string; }): DummyTaskStatus { - const taskId = options.url.split("/").pop() || generateDummyTaskId(); + const segments = options.url.split("/"); + // URL pattern: /task/{task_id}/status - task_id is second to last + const taskId = segments.at(-2) || generateDummyTaskId(); return createDummyTaskStatus(taskId); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/utils/dummyData.ts` around lines 128 - 133, The URL parsing in dummyTaskStatusFetcher incorrectly takes the last path segment (which is "status")—update dummyTaskStatusFetcher to extract the task_id from the URL pattern used in status.get.ts (/task/[r:task_id]/status) by parsing the path and selecting the segment before the final "status" (or use a regex to capture the value between "/task/" and "/status"); then pass that captured taskId into createDummyTaskStatus and only fall back to generateDummyTaskId() if the capture fails. Ensure you reference the dummyTaskStatusFetcher function and createDummyTaskStatus/generateDummyTaskId helpers when making the change.server/api/transcribe/submit.post.ts-49-50 (1)
49-50:⚠️ Potential issue | 🟠 MajorInconsistent usage: passing a value instead of a function to
withDummyFetcher.Other routes pass a fetcher function to
withDummyFetcher(e.g.,dummyTaskStatusFetcher,dummySummaryFetcher), but this route passes a pre-evaluated value. This means:
- The task ID is generated once at module load time, not per request
- All requests will receive the same dummy task ID
If the builder expects a function, this should be corrected for consistency and correct per-request behavior.
🐛 Proposed fix: use a function for per-request task ID generation
-.withDummyFetcher(createDummyTaskStatus(generateDummyTaskId())) +.withDummyFetcher(() => createDummyTaskStatus(generateDummyTaskId()))Alternatively, create a dedicated fetcher similar to
dummyTaskStatusFetcherthat doesn't require URL parsing.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/api/transcribe/submit.post.ts` around lines 49 - 50, The current call passes a pre-evaluated value to withDummyFetcher (using createDummyTaskStatus(generateDummyTaskId())) which generates a single task ID at module load and reuses it for all requests; change to pass a function that returns a fresh dummy status per request (e.g., provide a fetcher function that calls createDummyTaskStatus(generateDummyTaskId()) on each invocation) or implement a dedicated fetcher similar to dummyTaskStatusFetcher/dummySummaryFetcher and pass that to withDummyFetcher before calling build("/transcribe").app/plugins/cleanupTranscriptions.client.ts-1-11 (1)
1-11:⚠️ Potential issue | 🟠 MajorExtract
cleanupOldTranscriptions()as a standalone utility function.Calling
useTranscription()in a plugin context is problematic because the composable relies ononMountedandonUnmountedlifecycle hooks (lines 17-31 inuseTranscriptions.ts) which won't fire in a plugin context. Although thecleanupOldTranscriptions()function doesn't depend on the reactive state, using composables outside components is not recommended.Move
cleanupOldTranscriptions()to a separate utility module inapp/utils/and call it directly from the plugin:// app/utils/transcription.ts export async function cleanupOldTranscriptions(): Promise<number> { const thresholdDate = new Date(Date.now() - TRANSCRIPTION_RETENTION_PERIOD_MS); return await db.transcriptions .where("createdAt") .below(thresholdDate) .delete(); }Then update the plugin to import and call the utility directly instead of using the composable.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/plugins/cleanupTranscriptions.client.ts` around lines 1 - 11, Extract the cleanup logic out of the composable by creating a standalone exported async function cleanupOldTranscriptions in a new utility module (e.g., export async function cleanupOldTranscriptions(): Promise<number> { const thresholdDate = new Date(Date.now() - TRANSCRIPTION_RETENTION_PERIOD_MS); return db.transcriptions.where("createdAt").below(thresholdDate).delete(); }), then update the plugin's default export to import and call that utility cleanupOldTranscriptions directly (remove the useTranscription() call and its destructuring) and keep the same try/catch and logging (logger.info/logger.error) to report the deleted count or error.app/components/EditorModeSelector.vue-20-21 (1)
20-21:⚠️ Potential issue | 🟠 Major
containerRefis declared but never bound in template.The
containerRefis declared but not assigned to any element viarefattribute in the template. TheupdateIndicatorfunction relies on it to calculate button positions relative to the container.🐛 Proposed fix - add ref binding to container
You need to wrap the buttons in a container element and bind the ref. For example, in the template around the
UButtonMotionloop:<UFieldGroup> + <div ref="containerRef" class="flex"> <UButtonMotion v-for="(m, index) in modes" ... </UButtonMotion> + </div> </UFieldGroup>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/EditorModeSelector.vue` around lines 20 - 21, containerRef is declared but never bound, so updateIndicator can't compute positions; bind the ref in the template by wrapping the UButtonMotion loop in a container element (e.g., a div) and add ref="containerRef" (matching the containerRef ref variable) so updateIndicator can read container dimensions relative to buttonRefs; ensure button elements (tracked by buttonRefs) remain children of that container so position calculations are correct.app/components/MediaPreviewView.vue-132-133 (1)
132-133:⚠️ Potential issue | 🟠 MajorMemory leak: Object URL not revoked.
URL.createObjectURLallocates memory that persists until explicitly released. Without cleanup, switching media or unmounting the component leaks blob URLs.🐛 Proposed fix using watchEffect for automatic cleanup
-const mediaSource = computed(() => URL.createObjectURL(input.value.media)); +const mediaSource = ref<string>(''); + +watchEffect((onCleanup) => { + const url = URL.createObjectURL(input.value.media); + mediaSource.value = url; + onCleanup(() => URL.revokeObjectURL(url)); +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaPreviewView.vue` around lines 132 - 133, The mediaSource computed uses URL.createObjectURL but never revokes it, causing a memory leak; replace the computed mediaSource with a reactive ref and create the object URL inside a watchEffect (or watch on input.value.media) that assigns URL.createObjectURL(input.value.media) to mediaSource, revoke the previous URL via URL.revokeObjectURL(prev) whenever the media changes, and also revoke the current URL in onBeforeUnmount; reference the existing symbols mediaSource, isVideo, input, and use watchEffect/onBeforeUnmount to manage lifecycle and cleanup.app/components/media/TimelineView.client.vue-289-292 (1)
289-292:⚠️ Potential issue | 🟠 MajorUse a clamped start value consistently when building update payloads.
endis currently computed from rawnewStart; ifnewStartgoes negative whilestartis clamped to0, payload can become inconsistent.Suggested patch
- executeCommand( - new UpdateSegmentCommand(segmentId, { - start: Math.max(0, newStart), - end: Math.max(newStart + 0.5, newEnd), - }), - ); + const clampedStart = Math.max(0, newStart); + executeCommand( + new UpdateSegmentCommand(segmentId, { + start: clampedStart, + end: Math.max(clampedStart + 0.5, newEnd), + }), + );Apply the same change in both handlers.
Also applies to: 319-322
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/TimelineView.client.vue` around lines 289 - 292, The payload uses a clamped start for start but computes end from raw newStart, causing inconsistent ranges when newStart is negative; change the handler(s) that construct the UpdateSegmentCommand (referencing UpdateSegmentCommand and segmentId) to compute a clampedStart first (e.g., clampedStart = Math.max(0, newStart)) and then use clampedStart for both start and for the end calculation (e.g., end = Math.max(clampedStart + 0.5, newEnd)); apply the same fix in both places mentioned (the handler at the shown snippet and the other one around the second occurrence).app/components/media/TimelineView.client.vue-39-40 (1)
39-40:⚠️ Potential issue | 🟠 MajorAvoid direct mutation of prop-backed segment objects.
segmentsnow referencesprops.transcription.segments; downstream keyboard move logic mutates segment objects directly, bypassing command handling and one-way data flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/TimelineView.client.vue` around lines 39 - 40, The computed property segments currently returns a direct reference to props.transcription.segments, allowing downstream keyboard move logic to mutate prop-backed segment objects; instead have segments produce new objects (e.g., props.transcription.segments.map(s => ({ ...s })) or a deep clone) so mutations don't alter props directly, and update any keyboard move handler (the keyboard move logic) to emit an update or call the existing command/handler that performs sanctioned updates (such as emitting an update event or invoking the mutation command) rather than mutating the cloned objects in place.app/components/TimelineEditor.vue-34-39 (1)
34-39:⚠️ Potential issue | 🟠 MajorReset timeline state when no media is present.
Returning early without reset can leave stale
duration/timeRangefrom a previous transcription.Suggested patch
if (!currentTranscription?.mediaFile) { + duration.value = 0; + timeRange.value = [0, 0]; return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/TimelineEditor.vue` around lines 34 - 39, The initializeDuration function in TimelineEditor.vue exits early when props.transcription has no media, which leaves stale duration/timeRange from a previous transcription; update the early-return path to reset the timeline state (e.g., set the component state/refs for duration to 0 and timeRange to a zero-range like {start:0, end:0} and clear any selection/highlight) or invoke an existing resetTimelineState() helper before returning, so duration/timeRange are always cleared when no mediaFile is present.app/composables/useTranscriptions.ts-67-75 (1)
67-75:⚠️ Potential issue | 🟠 MajorHarden update semantics: keep immutable fields immutable and always bump
updatedAt.
updateTranscriptioncurrently accepts fullPartial<StoredTranscription>, so callers can submitid/createdAt, and updates can land without refreshingupdatedAt.Suggested patch
- async function updateTranscription( - id: string, - updates: Partial<StoredTranscription>, - ) { - const updatesParsed = StoredTranscriptionSchema.partial().parse({ - ...updates, - }); - await db.transcriptions.update(id, updatesParsed); - } + async function updateTranscription( + id: string, + updates: Partial<Omit<StoredTranscription, "id" | "createdAt" | "updatedAt">>, + ) { + const updatesParsed = StoredTranscriptionSchema + .omit({ id: true, createdAt: true, updatedAt: true }) + .partial() + .parse(updates); + + await db.transcriptions.update(id, { + ...updatesParsed, + updatedAt: new Date(), + }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/useTranscriptions.ts` around lines 67 - 75, The updateTranscription function must prevent callers from changing immutable fields and always refresh updatedAt: strip id and createdAt from the incoming updates before validating with StoredTranscriptionSchema.partial(), then merge in an updatedAt timestamp (e.g., new Date().toISOString()) into the parsed updates and pass that to db.transcriptions.update; ensure you still validate/parse via StoredTranscriptionSchema.partial() but only on mutable fields so id/createdAt cannot be written and updatedAt is always bumped.app/pages/task/[taskId].vue-47-64 (1)
47-64:⚠️ Potential issue | 🟠 MajorAdd cleanup for task polling on component unmount.
The
pollTaskStatusfunction polling is timer-based (5-second intervals) and does not return a cancel/disposer function, so it continues running after the component unmounts. The callbacks will mutate stale component state (progression.value,errorMessage.value).Modify
pollTaskStatusto return a cancel function, store it in the component, and call it inonUnmounted:// In useTaskListener.ts, have pollTaskStatus return a cancel function: return () => { /* set flag to break polling loop */ } // In [taskId].vue: onMounted(async () => { // ... const cancel = pollTaskStatus(...) onUnmounted(() => cancel()) })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/pages/task/`[taskId].vue around lines 47 - 64, pollTaskStatus currently starts a timer-based poll that mutates component state (progression.value, errorMessage.value) but doesn't return a disposer, so polling continues after unmount; update pollTaskStatus (in useTaskListener.ts) to return a cancel function that sets a flag to break the polling loop and stops any timers, then in the component ([taskId].vue) capture the returned cancel from pollTaskStatus when you call it and call that cancel inside onUnmounted to prevent callbacks (which call progression.value updates and applyTaskResult) from running after the component is torn down.app/composables/transcriptionService.ts-81-96 (1)
81-96:⚠️ Potential issue | 🟠 MajorIncorrect insertion index for "after" direction.
The
splicealways inserts attargetIndex, but whendirection === "after", the new segment should be inserted attargetIndex + 1. Currently, inserting "after" actually inserts "before" the target segment.🐛 Proposed fix
+ const insertIndex = command.direction === "after" ? targetIndex + 1 : targetIndex; - transcription.value.segments.splice(targetIndex, 0, newSegment); + transcription.value.segments.splice(insertIndex, 0, newSegment);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/transcriptionService.ts` around lines 81 - 96, The splice uses targetIndex for insertion regardless of direction causing "after" inserts to land before the target; change the insertion index used by transcription.value.segments.splice to compute insertIndex = targetIndex + (command.direction === "after" ? 1 : 0) and call splice(insertIndex, 0, newSegment) instead (keep existing start calculation, uuid(), newSegment, and command.setUndoCommand(new DeleteSegmentCommand(newSegment.id)) as-is).
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (100)
.env.example.github/workflows/ci.yml.github/workflows/docker-publish.yml.nuxtrcAGENTS.mdapp/app.vueapp/components/AudioRecordingView.client.vueapp/components/EditorModeSelector.vueapp/components/ExportToolbar.vueapp/components/HContainer.vueapp/components/KeyboardShortcutsHint.vueapp/components/LoadingView.vueapp/components/MediaEditor.vueapp/components/MediaPlaybackBar.vueapp/components/MediaPreviewView.vueapp/components/MediaProcessingView.vueapp/components/MediaProgressView.vueapp/components/MediaSelectionView.vueapp/components/NavigationMenu.vueapp/components/RenameSpeakerView.vueapp/components/SpeakerStatisticsView.vueapp/components/TimelineEditor.vueapp/components/TranscriptionEditView.vueapp/components/TranscriptionInfoView.vueapp/components/TranscriptionSummaryView.vueapp/components/TranscriptionViewer.vueapp/components/UploadMediaView.client.vueapp/components/media/CurrentSegementEditor.vueapp/components/media/TimelineView.client.vueapp/components/media/VideoView.vueapp/components/transcription/ProcessingTasksTable.vueapp/components/transcription/TranscriptionTable.vueapp/components/transcriptionList/TranscriptionList.vueapp/components/transcriptionList/TranscriptionSegmentEdit.vueapp/composables/audio_convertion.tsapp/composables/currentTranscription.tsapp/composables/export.tsapp/composables/transcriptionService.tsapp/composables/useAudioExtract.tsapp/composables/useAudioUpload.tsapp/composables/useDateFormatter.tsapp/composables/useSpectrogramGenerator.tsapp/composables/useTaskListener.tsapp/composables/useTaskStatus.tsapp/composables/useTasks.tsapp/composables/useTranscriptionSummary.tsapp/composables/useTranscriptions.tsapp/layouts/default.vueapp/pages/index.vueapp/pages/task/[taskId].vueapp/pages/transcription/[transcriptionId].vueapp/pages/transcription/index.vueapp/plugins/cleanupTranscriptions.client.tsapp/services/indexDbService.tsapp/stores/db.tsapp/stores/tasksStore.tsapp/stores/transcriptionsStore.tsapp/types/commands.tsapp/types/mediaProgress.tsapp/types/mediaStepInOut.tsapp/types/task.tsapp/types/transcriptionResponse.tsapp/utils/animationPresets.tsapp/utils/makrdownToDox.client.tsapp/utils/speakerStatistics.tsapp/utils/videoUtils.tsbiome.jsoni18n/locales/de.jsoni18n/locales/en.jsonnuxt.config.tspackage.jsonserver/api/health/liveness.get.tsserver/api/health/readiness.get.tsserver/api/health/startup.get.tsserver/api/summarize/submit.post.tsserver/api/transcribe/[task_id]/index.get.tsserver/api/transcribe/[task_id]/status.get.tsserver/api/transcribe/submit.post.tsserver/assets/changelogs/v0.5.0.mdserver/assets/changelogs/v0.6.0.mdserver/plugins/startup-log.tsserver/utils/apiHanlder.tsserver/utils/dummyData.tsserver/utils/verboseFetch.tsshared/types/summary.tstests/composables/dialog.test.tstests/composables/speakerColor.test.tstests/composables/useDateFormatter.test.tstests/composables/useTaskStatus.test.tstests/setup/vitest-setup.tstests/types/task.test.tstests/types/transcriptionResponse.test.tstests/unit/services/colorMapService.test.tstests/unit/services/indexDbService.test.tstests/unit/utils/httpErrorCode.test.tstests/unit/utils/math.test.tstests/unit/utils/speakerUtils.test.tstests/unit/utils/time.test.tstests/unit/utils/videoUtils.test.tsvitest.config.ts
💤 Files with no reviewable changes (9)
- app/stores/tasksStore.ts
- server/api/health/readiness.get.ts
- app/composables/currentTranscription.ts
- app/services/indexDbService.ts
- server/api/health/liveness.get.ts
- server/api/health/startup.get.ts
- app/stores/transcriptionsStore.ts
- server/utils/verboseFetch.ts
- server/plugins/startup-log.ts
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
i18n/locales/en.json (2)
336-340:⚠️ Potential issue | 🟡 MinorMissing language entry:
kn(Kannada) is present inde.jsonbut missing here.The German locale file includes
"kn": "Kannada"between"sl"and"et", but it's absent from the English locale. This inconsistency could cause issues if the UI references this language code.🐛 Proposed fix - add missing entry
"az": "Azerbaijani", "sl": "Slovenian", + "kn": "Kannada", "et": "Estonian",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/locales/en.json` around lines 336 - 340, Add the missing Kannada entry to the English locale by inserting the key/value pair "kn": "Kannada" into i18n/locales/en.json alongside the other language mappings (e.g., between "sl": "Slovenian" and "et": "Estonian") so the English locale matches de.json and prevents missing-key lookups for the "kn" language code.
386-386:⚠️ Potential issue | 🟡 MinorIncorrect language mapping:
bashould be "Bashkir", not "Basque".The ISO 639-1 code
bacorresponds to Bashkir (a Turkic language), not Basque. Basque is already correctly mapped toeuon line 341. The German locale file correctly has"ba": "Baschkirisch".🐛 Proposed fix
- "ba": "Basque", + "ba": "Bashkir",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/locales/en.json` at line 386, The locale mapping for the key "ba" in en.json is wrong; update the value for the JSON key "ba" from "Basque" to "Bashkir" so the ISO 639-1 code correctly maps to Bashkir (the key "eu" already maps to Basque).app/components/ExportToolbar.vue (1)
75-80:⚠️ Potential issue | 🟡 MinorUpdate summary scope hint text to match actual behavior.
Line 79 uses
t('export.textOnly'), but summary is also included in DOCX export now. This creates a UX mismatch in the options panel.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/ExportToolbar.vue` around lines 75 - 80, The UX text in ExportToolbar.vue is misleading: inside the summary toggle block (v-if="props.transcription.summary") the hint span calls t('export.textOnly') but summaries are now included in DOCX exports too; update that translation key or message so it no longer says "text only" — e.g., replace t('export.textOnly') with a more accurate key/message (such as t('export.summaryIncluded') or t('export.textAndDocx')) and update the translations accordingly so the span under the summary toggle reflects that summaries are included in DOCX as well.
🧹 Nitpick comments (5)
i18n/locales/de.json (1)
173-175: Inconsistent key naming convention forTaskNotFound.The key
TaskNotFounduses PascalCase while all other error keys in this section (and throughout the file) use camelCase (e.g.,noMediaFile,failedToLoad,noResult). Consider renaming totaskNotFoundfor consistency.🔧 Suggested fix
- "TaskNotFound": "Aufgabe nicht gefunden" + "taskNotFound": "Aufgabe nicht gefunden"Note: This change would also need to be applied to
en.jsonand any code referencing this key.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/locales/de.json` around lines 173 - 175, Rename the inconsistent JSON key "TaskNotFound" to camelCase "taskNotFound" in the locale entry (replace the key in the de.json snippet where "noResult" and "TaskNotFound" appear), then update the corresponding key in en.json and any code that references "TaskNotFound" (search for occurrences of TaskNotFound in the codebase and change them to taskNotFound) to keep naming consistent across locales and usage.i18n/locales/en.json (1)
173-175: Inconsistent key naming convention forTaskNotFound.Same issue as in
de.json- this key uses PascalCase while all other error keys use camelCase. Consider renaming totaskNotFoundfor consistency.🔧 Suggested fix
- "TaskNotFound": "Task not found" + "taskNotFound": "Task not found"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@i18n/locales/en.json` around lines 173 - 175, Rename the inconsistent PascalCase key "TaskNotFound" to camelCase "taskNotFound" to match the existing error key convention; update any references in code or other locale files that currently use "TaskNotFound" (search for TaskNotFound) to use "taskNotFound" so lookups (e.g., i18n.get('taskNotFound') or similar) continue to work consistently across locales.app/components/MediaPlaybackBar.vue (1)
39-63: RedundantloadMedia()call on mount.Both
onMounted(line 40) and the watcher with{ immediate: true }(line 62) callloadMedia()on initial mount, causing double execution. The watcher alone is sufficient sinceimmediate: truetriggers it immediately when the component is created.♻️ Suggested fix
-onMounted(() => { - loadMedia(); -}); - onUnmounted(() => { if (mediaSrc.value) { URL.revokeObjectURL(mediaSrc.value); } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaPlaybackBar.vue` around lines 39 - 63, The onMounted hook is redundantly calling loadMedia() while the watch on props.transcription already calls loadMedia() with { immediate: true }, causing double initialization; remove the onMounted(() => { loadMedia(); }) block so loadMedia() is only invoked by the watcher (retain onUnmounted and the watcher as-is), ensuring you still call loadMedia() on initial mount via the immediate watcher and keep URL.revokeObjectURL in onUnmounted.app/composables/export.ts (2)
95-101: Extract a shareddownloadBlobhelper to remove duplication and harden cleanup.The same anchor/object-URL flow is repeated in five places. Centralizing this reduces drift and makes cleanup behavior consistent.
♻️ Proposed refactor
+function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + try { + a.click(); + } finally { + setTimeout(() => URL.revokeObjectURL(url), 0); + } +} -const blob = new Blob([finalText], { type: "text/plain" }); -const url = URL.createObjectURL(blob); -const a = document.createElement("a"); -a.href = url; -a.download = `${options.transcription.name}.txt`; -a.click(); -URL.revokeObjectURL(url); +const blob = new Blob([finalText], { type: "text/plain" }); +downloadBlob(blob, `${options.transcription.name}.txt`);Also applies to: 130-137, 163-169, 203-210, 217-223
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/export.ts` around lines 95 - 101, Extract the repeated anchor/URL flow into a single helper function named downloadBlob(blob: Blob, filename: string) (or export const downloadBlob) and replace the five inline copies with calls to it; the helper should create an object URL from the blob, create a hidden anchor, set href/download, append to document, call click(), remove the anchor, and revoke the object URL (ensure revokeObjectURL runs in a finally or after a short tick to guarantee download start). Update callers that currently construct Blob and set a.download = `${options.transcription.name}.txt` (and the other four occurrences) to pass the blob and filename into downloadBlob so cleanup behavior is consistent across sendExportText / export functions.
119-122: Renametransciption→transcriptionfor readability and consistency.This is non-functional, but the typo appears in several signatures/usages (e.g., Line 119, Line 143, Line 212) and makes the API surface harder to scan.
Also applies to: 134-134, 143-150, 166-166, 212-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/composables/export.ts` around lines 119 - 122, Rename the misspelled parameter and all its usages from "transciption" to "transcription" to improve readability and API consistency: update the function signatures and internal references (e.g., where you access transciption.segments) in export-related functions in this file (notably the functions that accept StoredTranscription and the call sites referenced around the previous signatures/usages), ensuring the type annotation StoredTranscription is preserved and all occurrences at the noted locations (around the signatures and usages) are renamed so the identifier matches "transcription" everywhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/MediaPlaybackBar.vue`:
- Around line 140-145: The UI renders segment.speaker directly which can show
"null" or "undefined"; update the rendering in the v-for for currentSegments to
use a fallback (e.g., segment.speaker ?? 'unknown') wherever speaker is
displayed so it matches the fallback used when calling getSpeakerColor — locate
the loop over currentSegments and the spans that reference segment.speaker and
replace those uses with the null-coalesced fallback to ensure consistent text
and color behavior.
- Line 124: Remove the stray text "valid Tailwind CSS class and will no" that
sits between the closing </script> and the opening <template> in
MediaPlaybackBar.vue; ensure the SFC has only the </script> tag followed
immediately (or after valid whitespace/comments) by the <template> block so the
file compiles.
- Around line 80-95: The togglePlay function currently flips isPlaying manually
which can desync if play() rejects or media ends; instead wire media events and
update state from them: add onPlay and onPause (or onEnded) handlers that set
isPlaying.value = true/false, attach them to the video and audio elements (e.g.,
`@play`="onPlay" `@pause`="onPause" `@ended`="onPause"), and modify togglePlay to call
play()/pause() but not directly flip isPlaying; also handle the Promise from
play() (catch errors and avoid setting isPlaying) so state is only driven by the
media events and success/failure of play().
In `@app/components/UploadMediaView.client.vue`:
- Line 239: The UFileUpload label is hardcoded in German; update
UploadMediaView.client.vue to call the component's translation function
(useI18n/t) instead of the literal string for the label prop on UFileUpload, and
add the new translation key (e.g., upload.dragOrClickToUpload) with German and
other locales to your locale files so the label is resolved via
t('upload.dragOrClickToUpload') where UFileUpload is rendered.
- Around line 238-239: UFileUpload is not bound to any data and loadAudio
expects an Event; update the component to use v-model bound to a reactive File
(e.g., selectedMediaFile) instead of relying on fileInputRef, remove any
fileInputRef usage, and change the loadAudio signature from accepting Event to
accepting a File (e.g., loadAudio(mediaFile: File): Promise<void>) and operate
on that File directly; also replace the hardcoded German label with an i18n
lookup (use this.$t or useNuxtApp().$t with a new translation key similar to
MediaSelectionView.vue) so file selection works and text is localized.
---
Outside diff comments:
In `@app/components/ExportToolbar.vue`:
- Around line 75-80: The UX text in ExportToolbar.vue is misleading: inside the
summary toggle block (v-if="props.transcription.summary") the hint span calls
t('export.textOnly') but summaries are now included in DOCX exports too; update
that translation key or message so it no longer says "text only" — e.g., replace
t('export.textOnly') with a more accurate key/message (such as
t('export.summaryIncluded') or t('export.textAndDocx')) and update the
translations accordingly so the span under the summary toggle reflects that
summaries are included in DOCX as well.
In `@i18n/locales/en.json`:
- Around line 336-340: Add the missing Kannada entry to the English locale by
inserting the key/value pair "kn": "Kannada" into i18n/locales/en.json alongside
the other language mappings (e.g., between "sl": "Slovenian" and "et":
"Estonian") so the English locale matches de.json and prevents missing-key
lookups for the "kn" language code.
- Line 386: The locale mapping for the key "ba" in en.json is wrong; update the
value for the JSON key "ba" from "Basque" to "Bashkir" so the ISO 639-1 code
correctly maps to Bashkir (the key "eu" already maps to Basque).
---
Nitpick comments:
In `@app/components/MediaPlaybackBar.vue`:
- Around line 39-63: The onMounted hook is redundantly calling loadMedia() while
the watch on props.transcription already calls loadMedia() with { immediate:
true }, causing double initialization; remove the onMounted(() => { loadMedia();
}) block so loadMedia() is only invoked by the watcher (retain onUnmounted and
the watcher as-is), ensuring you still call loadMedia() on initial mount via the
immediate watcher and keep URL.revokeObjectURL in onUnmounted.
In `@app/composables/export.ts`:
- Around line 95-101: Extract the repeated anchor/URL flow into a single helper
function named downloadBlob(blob: Blob, filename: string) (or export const
downloadBlob) and replace the five inline copies with calls to it; the helper
should create an object URL from the blob, create a hidden anchor, set
href/download, append to document, call click(), remove the anchor, and revoke
the object URL (ensure revokeObjectURL runs in a finally or after a short tick
to guarantee download start). Update callers that currently construct Blob and
set a.download = `${options.transcription.name}.txt` (and the other four
occurrences) to pass the blob and filename into downloadBlob so cleanup behavior
is consistent across sendExportText / export functions.
- Around line 119-122: Rename the misspelled parameter and all its usages from
"transciption" to "transcription" to improve readability and API consistency:
update the function signatures and internal references (e.g., where you access
transciption.segments) in export-related functions in this file (notably the
functions that accept StoredTranscription and the call sites referenced around
the previous signatures/usages), ensuring the type annotation
StoredTranscription is preserved and all occurrences at the noted locations
(around the signatures and usages) are renamed so the identifier matches
"transcription" everywhere.
In `@i18n/locales/de.json`:
- Around line 173-175: Rename the inconsistent JSON key "TaskNotFound" to
camelCase "taskNotFound" in the locale entry (replace the key in the de.json
snippet where "noResult" and "TaskNotFound" appear), then update the
corresponding key in en.json and any code that references "TaskNotFound" (search
for occurrences of TaskNotFound in the codebase and change them to taskNotFound)
to keep naming consistent across locales and usage.
In `@i18n/locales/en.json`:
- Around line 173-175: Rename the inconsistent PascalCase key "TaskNotFound" to
camelCase "taskNotFound" to match the existing error key convention; update any
references in code or other locale files that currently use "TaskNotFound"
(search for TaskNotFound) to use "taskNotFound" so lookups (e.g.,
i18n.get('taskNotFound') or similar) continue to work consistently across
locales.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (9)
app/components/EditorModeSelector.vueapp/components/ExportToolbar.vueapp/components/MediaPlaybackBar.vueapp/components/MediaProcessingView.vueapp/components/UploadMediaView.client.vueapp/composables/export.tsapp/composables/useAudioUpload.tsi18n/locales/de.jsoni18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (3)
- app/composables/useAudioUpload.ts
- app/components/EditorModeSelector.vue
- app/components/MediaProcessingView.vue
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (4)
app/components/media/VideoView.vue (2)
12-13: Remove misleading comment.The comment states "Import useI18n composable" but there's no import statement —
useI18nis auto-imported by Nuxt. Either remove the comment or add a clarifying note about auto-imports.✏️ Suggested fix
-// Import useI18n composable const { t } = useI18n();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/VideoView.vue` around lines 12 - 13, The inline comment "Import useI18n composable" is misleading because useI18n is auto-imported by Nuxt; update the comment or remove it: locate the line with "const { t } = useI18n();" and either delete the misleading comment or replace it with a clarifying note such as "useI18n is auto-imported by Nuxt" so the code around useI18n and t remains accurate and clear.
68-84: Handle potentialplay()rejection.The
play()method returns a Promise that can reject (e.g., due to browser autoplay policies). Currently,isPlayingis toggled unconditionally on line 83, which could cause UI state desynchronization if playback fails.🛡️ Suggested improvement
const togglePlay = (): void => { if (videoElement.value) { if (isPlaying.value) { videoElement.value.pause(); + isPlaying.value = false; } else { - videoElement.value.play(); + videoElement.value.play().then(() => { + isPlaying.value = true; + }).catch((e) => { + console.warn('Playback failed:', e); + }); } } else if (audioElement.value) { if (isPlaying.value) { audioElement.value.pause(); + isPlaying.value = false; } else { - audioElement.value.play(); + audioElement.value.play().then(() => { + isPlaying.value = true; + }).catch((e) => { + console.warn('Playback failed:', e); + }); } } - - isPlaying.value = !isPlaying.value; };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/media/VideoView.vue` around lines 68 - 84, togglePlay toggles isPlaying unconditionally which can desync UI if videoElement.play()/audioElement.play() rejects; make togglePlay async, await the play() Promise inside a try/catch and only set isPlaying.value = true after play() resolves, while for pauses set isPlaying.value = false immediately; handle both videoElement and audioElement branches, catch and log/playback-failure (do not flip isPlaying on failed play), and keep references to togglePlay, videoElement, audioElement, and isPlaying when applying the change.app/components/MediaPreviewView.vue (1)
28-130: Consider usingcomputedfor i18n-reactive options.The
audioLanguageOptionsusesref()witht()calls at setup time. If the locale changes while this component is mounted, the labels won't update. Usingcomputedwould make labels reactive to locale changes.♻️ Suggested refactor for i18n reactivity
-const audioLanguageOptions = ref<SelectMenuItem[]>([ +const audioLanguageOptions = computed<SelectMenuItem[]>(() => [ { label: t("upload.autoDetection"), value: "auto" }, { label: t("languages.de"), value: "de" }, // ... rest of options -]); +]);The same applies to
speakerOptionson lines 19-27.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MediaPreviewView.vue` around lines 28 - 130, audioLanguageOptions is initialized with ref([...]) calling t() at setup time so its labels won't update when locale changes; change audioLanguageOptions to a computed that returns the array (i.e. computed(() => [ { label: t(...), value: ... }, ... ])) so t() is re-evaluated on locale changes, and apply the same change to speakerOptions (replace ref([...]) with computed(() => [...]) and keep the same item shapes).app/components/NavigationMenu.vue (1)
8-18: Remove the commented legacy navigation block.Line 8 through Line 18 keeps obsolete template code in comments; please delete it to avoid drift and confusion.
🧹 Proposed cleanup
- <!-- <ClientOnly> - <UNavigationMenu content-orientation="vertical" variant="link" :items="items" - class="w-full grid grid-cols-3 items-center z-50 [&>*:nth-child(1)]:justify-self-start [&>*:nth-child(2)]:justify-self-center [&>*:nth-child(3)]:justify-self-end"> - <template `#disclaimer`> - <DisclaimerButton variant="ghost" /> - </template> -</UNavigationMenu> -<template `#fallback`> - <div class="w-full h-12 bg-gray-100 animate-pulse rounded"></div> - </template> -</ClientOnly> -->🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/NavigationMenu.vue` around lines 8 - 18, Remove the obsolete commented-out legacy navigation block: delete the multi-line comment that contains the <ClientOnly> wrapper with <UNavigationMenu content-orientation="vertical" ...>, the <template `#disclaimer`> with <DisclaimerButton>, and the <template `#fallback`> skeleton div so the component no longer contains that dead commented code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/components/media/VideoView.vue`:
- Around line 147-154: The subtitle display is inconsistent: getSpeakerColor
uses segment.speaker ?? 'unknown' but the rendered label uses segment.speaker
directly, which can show empty/undefined; update the rendering to use the same
fallback so the displayed speaker matches the color logic. Locate the v-for loop
over currentSegments in VideoView.vue (the div with class "subtitle-segment")
and change the speaker display expression to use the same fallback
(segment.speaker ?? 'unknown') so both getSpeakerColor(...) and the shown
speaker text are consistent.
- Around line 134-137: The <video> source currently hardcodes type="video/mp4",
which can mismatch actual file MIME types; update the <source> element in
VideoView.vue to bind its type to the actual media type (use mediaFile.type with
a safe fallback like mediaFile.type || 'video/mp4') so mediaSrc and the video
player use the correct MIME; ensure this change is applied where isVideoFile and
mediaFile are checked and keeps refs such as videoElement and handlers
onTimeUpdate and togglePlay untouched.
- Line 37: Rename the misspelled handler function handleTooglePlayCommand to
handleTogglePlayCommand and update all references: change the registration call
registerHandler(Cmds.TogglePlayCommand, handleTooglePlayCommand) to
registerHandler(Cmds.TogglePlayCommand, handleTogglePlayCommand), rename the
function declaration/definition named handleTooglePlayCommand to
handleTogglePlayCommand, and update any other invocations or exports/imports
that reference handleTooglePlayCommand so they match the corrected name.
- Around line 86-103: The watch callback creates object URLs via
URL.createObjectURL for props.transcription.mediaFile (assigning to
mediaSrc.value) but never revokes them; modify the code to call
URL.revokeObjectURL on the previous mediaSrc.value before overwriting it inside
the watch (and set mediaSrc.value to null/'' after revocation if desired), and
also add an onUnmounted handler that revokes the current mediaSrc.value when the
component is destroyed; reference the watch on props.transcription,
mediaSrc.value, mediaFile.value, URL.createObjectURL, URL.revokeObjectURL, and
add onUnmounted cleanup.
In `@app/components/MediaPreviewView.vue`:
- Around line 135-141: formatFileSize currently computes index i using Math.log
and looks up sizes = ["Bytes","KB","MB","GB"], causing sizes[i] to be undefined
for >=1 TB; update formatFileSize to either extend the sizes array to include
"TB" (and larger units if desired) or clamp i to sizes.length - 1 before
indexing so large byte counts map to the largest available unit, and ensure the
divisor uses k ** i accordingly; modify only the formatFileSize function to
implement the clamp or extended units.
- Around line 132-133: The computed mediaSource currently calls
URL.createObjectURL(input.value.media) but never revokes it, causing a memory
leak; update the component to store the generated blob URL in a ref (e.g.,
blobUrlRef), create the object URL inside a watch on input.value.media (or
inside a computed fallback) and call URL.revokeObjectURL on the previous blob
URL before assigning a new one, and also revoke the current blob URL in
onUnmounted; reference the existing symbols mediaSource, isVideo, and
input.value.media and ensure you import/use watch and onUnmounted so the blob
URL is cleaned up on media changes and component teardown.
---
Nitpick comments:
In `@app/components/media/VideoView.vue`:
- Around line 12-13: The inline comment "Import useI18n composable" is
misleading because useI18n is auto-imported by Nuxt; update the comment or
remove it: locate the line with "const { t } = useI18n();" and either delete the
misleading comment or replace it with a clarifying note such as "useI18n is
auto-imported by Nuxt" so the code around useI18n and t remains accurate and
clear.
- Around line 68-84: togglePlay toggles isPlaying unconditionally which can
desync UI if videoElement.play()/audioElement.play() rejects; make togglePlay
async, await the play() Promise inside a try/catch and only set isPlaying.value
= true after play() resolves, while for pauses set isPlaying.value = false
immediately; handle both videoElement and audioElement branches, catch and
log/playback-failure (do not flip isPlaying on failed play), and keep references
to togglePlay, videoElement, audioElement, and isPlaying when applying the
change.
In `@app/components/MediaPreviewView.vue`:
- Around line 28-130: audioLanguageOptions is initialized with ref([...])
calling t() at setup time so its labels won't update when locale changes; change
audioLanguageOptions to a computed that returns the array (i.e. computed(() => [
{ label: t(...), value: ... }, ... ])) so t() is re-evaluated on locale changes,
and apply the same change to speakerOptions (replace ref([...]) with computed(()
=> [...]) and keep the same item shapes).
In `@app/components/NavigationMenu.vue`:
- Around line 8-18: Remove the obsolete commented-out legacy navigation block:
delete the multi-line comment that contains the <ClientOnly> wrapper with
<UNavigationMenu content-orientation="vertical" ...>, the <template `#disclaimer`>
with <DisclaimerButton>, and the <template `#fallback`> skeleton div so the
component no longer contains that dead commented code.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge base: Disabled due to data retention organization setting
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
app/components/HContainer.vueapp/components/MediaPlaybackBar.vueapp/components/MediaPreviewView.vueapp/components/NavigationMenu.vueapp/components/TranscriptionInfoView.vueapp/components/media/VideoView.vuepackage.json
🚧 Files skipped from review as they are similar to previous changes (3)
- app/components/TranscriptionInfoView.vue
- app/components/HContainer.vue
- app/components/MediaPlaybackBar.vue
- Create languages utility with 60+ supported languages (de, en, fr, it at top) - Add optional language field to SummarizeRequest schema - Add language selector with flag icons to summary popover - Persist language selection using VueUse useStorage - Add i18n translations for language selection labels
No description provided.