Skip to content

Feature/UI redesing#163

Merged
swordbreaker merged 47 commits intomainfrom
feature/ui-redesing
Mar 3, 2026
Merged

Feature/UI redesing#163
swordbreaker merged 47 commits intomainfrom
feature/ui-redesing

Conversation

@swordbreaker
Copy link
Collaborator

No description provided.

swordbreaker and others added 28 commits February 9, 2026 18:45
- 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.
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This 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

Cohort / File(s) Summary
Configuration & Environment
.env.example, nuxt.config.ts, .nuxtrc, biome.json
Added environment variables (DUMMY, LOGGER_LAYER_URI), configured Nuxt layers and runtime config, updated i18n strategy to no_prefix, added Tailwind CSS parsing.
Workflow & CI/CD
.github/workflows/ci.yml, .github/workflows/docker-publish.yml
Renamed build job to ci, replaced Playwright config with dedicated test step, removed version_bump input and updated docker workflow reference.
Core State Management
app/stores/db.ts, app/composables/useTranscriptions.ts, app/composables/useTasks.ts, app/composables/currentTranscription.ts
Migrated from Pinia stores to Dexie-based database and new composables; removed legacy useCurrentTranscription.
Media Processing Composables
app/composables/useTaskListener.ts, app/composables/useTaskStatus.ts, app/composables/useDateFormatter.ts, app/composables/useAudioExtract.ts, app/composables/useAudioUpload.ts
Added task polling, status display utilities, date formatting, and improved audio extraction lifecycle management.
Transcription & Export Composables
app/composables/export.ts, app/composables/transcriptionService.ts, app/composables/useTranscriptionSummary.ts, app/composables/audio_convertion.ts
Refactored export to use transcription props, implemented summary generation, updated transcription service for prop-driven data, fixed composable naming typo.
Media Selection & Preview Components
app/components/MediaSelectionView.vue, app/components/MediaPreviewView.vue, app/components/MediaProcessingView.vue, app/components/MediaProgressView.vue
New components implementing stepped media workflow with file selection, language/speaker configuration, and progress tracking.
Media Playback & Editing Components
app/components/MediaPlaybackBar.vue, app/components/MediaEditor.vue, app/components/AudioRecordingView.client.vue, app/components/TimelineEditor.vue
Added playback bar with subtitles, refactored editor to use prop-driven transcription, simplified recording view, introduced timeline editor.
Transcription Display & Management
app/components/TranscriptionViewer.vue, app/components/TranscriptionEditView.vue, app/components/TranscriptionInfoView.vue, app/components/TranscriptionSummaryView.vue, app/components/SpeakerStatisticsView.vue
New and refactored components for viewing, editing, and analyzing transcriptions; added summary generation and speaker statistics.
Transcription List Components
app/components/transcriptionList/TranscriptionList.vue, app/components/transcriptionList/TranscriptionSegmentEdit.vue, app/components/media/CurrentSegementEditor.vue
Migrated to prop-driven architecture with command bus integration; added keyboard shortcuts and progress tracking.
Editor & Navigation Components
app/components/EditorModeSelector.vue, app/components/HContainer.vue, app/components/LoadingView.vue, app/components/NavigationMenu.vue, app/components/ExportToolbar.vue
New editor mode selector and container layouts, refactored navigation to slot-based approach, enhanced export toolbar with DOCX support.
Transcription Management Components
app/components/transcription/TranscriptionTable.vue, app/components/transcription/ProcessingTasksTable.vue
New table components for displaying transcriptions and processing tasks with sorting and status tracking.
Layout & Pages
app/layouts/default.vue, app/pages/index.vue, app/pages/task/[taskId].vue, app/pages/transcription/index.vue, app/pages/transcription/[transcriptionId].vue
Refactored index page with stepped media workflow, updated task page with polling, reimplemented transcription listing and detail pages using new composables.
Plugin & Service Layer
app/plugins/cleanupTranscriptions.client.ts, app/services/indexDbService.ts, app/stores/tasksStore.ts, app/stores/transcriptionsStore.ts
Removed legacy Pinia stores and manual IndexedDB service; added client plugin for transcription cleanup.
Server API Endpoints
server/api/transcribe/submit.post.ts, server/api/transcribe/[task_id]/status.get.ts, server/api/transcribe/[task_id]/index.get.ts, server/api/summarize/submit.post.ts, server/utils/apiHanlder.ts, server/utils/dummyData.ts
Refactored endpoints to use builder pattern (apiHandler), removed health checks, added dummy data support.
Type & Utility Fixes
app/types/commands.ts, app/types/transcriptionResponse.ts, app/types/task.ts, app/types/mediaProgress.ts, app/types/mediaStepInOut.ts
Fixed typos (Segement→Segment, Transcripton→Transcription), added media progress and step types.
Utilities
app/utils/makrdownToDox.client.ts, app/utils/speakerStatistics.ts, app/utils/videoUtils.ts, app/utils/animationPresets.ts
Added DOCX conversion, speaker statistics computation, video detection, and animation presets.
Internationalization
i18n/locales/de.json, i18n/locales/en.json
Added translations for media configuration, summary types, statistics, and editor modes.
Testing
tests/composables/*.test.ts, tests/unit/**/*.test.ts, tests/setup/vitest-setup.ts, vitest.config.ts
Comprehensive new test suites for composables, types, utilities, and services; added Vitest configuration.
Documentation & Dependencies
AGENTS.md, package.json, server/assets/changelogs/v0.6.0.md
Added comprehensive development guidelines, updated dependencies (Dexie, motion-v, testing tools), added changelog.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a pull request description explaining the UI redesign changes, objectives, and any breaking changes or migration notes for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature/UI redesing' is related to the changeset as it involves significant UI component restructuring, new layout components, and styling changes throughout the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/ui-redesing

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello, 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

  • Comprehensive UI Redesign: The transcription detail page has undergone a significant UI overhaul, introducing modern animations, improved layouts, and a more intuitive user experience across various editing and viewing modes.
  • New Media Playback Bar: A new collapsible media playback bar has been implemented, offering video/audio preview with live subtitles, enhancing the interactive transcription experience.
  • Advanced Summary Generation: Users can now generate summaries in three distinct formats: Short Protocol, Results Protocol, and Negotiation Protocol, catering to different reporting needs.
  • Speaker Statistics View: A dedicated speaker statistics view has been added, providing insights into speaking time distribution per speaker, complete with duration formatting and color-coded visualizations.
  • Enhanced Timeline Editor: The timeline editor has been improved with new functionalities for direct editing, including zoom, pan, and keyboard navigation, offering more precise control over transcription segments.
  • DOCX Export Functionality: Transcriptions and their generated summaries can now be exported as Word documents (.docx), with customizable options for including speakers, timestamps, and merged segments.
  • Refactored Data Management: The application's data persistence layer has been modernized by migrating from custom IndexedDB logic to Dexie, and introducing new composables (useTasks, useTranscriptions) for better state management and code organization.
  • Improved Media Processing Flow: The media upload and processing workflow has been streamlined into a multi-step process with clear progress indicators and enhanced error handling, providing better feedback to the user.
  • New Coding Guidelines and Testing Setup: Comprehensive coding guidelines (AGENTS.md) have been introduced, alongside a new Vitest-based testing setup, promoting code quality and maintainability.

🧠 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
  • AGENTS.md
    • Added comprehensive coding guidelines, project structure, dependencies, and Nuxt features documentation.
  • app/components/EditorModeSelector.vue
    • Added new component for selecting editor modes (view, summary, edit, statistics) with motion animations.
  • app/components/ExportToolbar.vue
    • Updated to accept a transcription prop and added exportAsDocx functionality.
    • Modified export handlers to directly use the transcription prop.
    • Included a new button for DOCX export.
  • app/components/HContainer.vue
    • Added new generic horizontal container component with slots for flexible content arrangement.
  • app/components/KeyboardShortcutsHint.vue
    • Added new component to display keyboard shortcuts with expand/collapse functionality and motion animations.
  • app/components/LoadingView.vue
    • Added new component for a stylized loading spinner with customizable text and motion animations.
  • app/components/MediaPlaybackBar.vue
    • Added new component for media playback controls, including video/audio preview, live subtitles, and expand/collapse functionality.
  • app/components/MediaPreviewView.vue
    • Added new component for media file preview and configuration settings (speakers, language) with motion animations.
  • app/components/MediaProcessingView.vue
    • Added new component for displaying media processing progress with animated steps and error handling.
  • app/components/MediaProgressView.vue
    • Added new component to visualize media processing steps with icons, messages, and progress bars.
  • app/components/MediaSelectionView.vue
    • Added new component for media upload or audio recording, featuring motion animations.
  • app/components/SpeakerStatisticsView.vue
    • Added new component to display speaker speaking time statistics with duration formatting and color-coded bars.
  • app/components/TimelineEditor.vue
    • Added new component for timeline editing, including zoom, pan, and keyboard navigation.
  • app/components/TranscriptionEditView.vue
    • Added new component for transcription editing, integrating MediaPlaybackBar, RenameSpeakerView, and TranscriptionList.
  • app/components/TranscriptionInfoView.vue
    • Added new component to display and edit transcription name, and download media, with popover functionality.
  • app/components/TranscriptionSummaryView.vue
    • Added new component for displaying and generating transcription summaries, supporting different summary types and export options.
  • app/components/transcription/ProcessingTasksTable.vue
    • Added new component for displaying a table of processing tasks.
  • app/components/transcription/TranscriptionTable.vue
    • Added new component for displaying a table of stored transcriptions.
  • app/composables/useDateFormatter.ts
    • Added new composable for consistent date formatting.
  • app/composables/useTaskListener.ts
    • Added new composable for polling task status and applying results.
  • app/composables/useTaskStatus.ts
    • Added new composable for task status display and color.
  • app/composables/useTasks.ts
    • Added new composable for managing tasks in IndexedDB using Dexie.
  • app/composables/useTranscriptionSummary.ts
    • Added new composable for generating and managing transcription summaries.
  • app/composables/useTranscriptions.ts
    • Added new composable for managing transcriptions in IndexedDB using Dexie.
  • app/plugins/cleanupTranscriptions.client.ts
    • Added new plugin for client-side transcription cleanup.
  • app/stores/db.ts
    • Added new file defining the Dexie database schema for tasks and transcriptions.
  • app/types/mediaProgress.ts
    • Added new type definition for media processing progress.
  • app/types/mediaStepInOut.ts
    • Added new type definitions for media selection and configuration data.
  • app/utils/animationPresets.ts
    • Added new file containing animation presets for consistent UI motion.
  • app/utils/makrdownToDox.client.ts
    • Added new utility for converting markdown content to DOCX file blobs.
  • app/utils/speakerStatistics.ts
    • Added new utility for computing and formatting speaker statistics.
  • app/utils/videoUtils.ts
    • Added new utility function to check if a given file is a video.
  • server/assets/changelogs/v0.6.0.md
    • Added new changelog entry for version 0.6.0, detailing UI redesign and new features.
  • server/utils/apiHanlder.ts
    • Added new utility for API route handling, including client UUID and dummy data integration.
  • server/utils/dummyData.ts
    • Added new utility for generating dummy data for tasks, transcriptions, and summaries for development and testing.
  • shared/types/summary.ts
    • Added new type definitions for summary types and request schema.
  • tests/composables/dialog.test.ts
    • Added new test file for the dialog composable.
  • tests/composables/speakerColor.test.ts
    • Added new test file for the speaker color composable.
  • tests/composables/useDateFormatter.test.ts
    • Added new test file for the date formatter composable.
  • tests/composables/useTaskStatus.test.ts
    • Added new test file for the task status composable.
  • tests/setup/vitest-setup.ts
    • Added new Vitest setup file with global mocks for IndexedDB, matchMedia, ResizeObserver, and IntersectionObserver.
  • tests/types/task.test.ts
    • Added new test file for task-related types and schemas.
  • tests/types/transcriptionResponse.test.ts
    • Added new test file for transcription response types and schemas.
  • tests/unit/services/colorMapService.test.ts
    • Added new test file for color map services.
  • tests/unit/services/indexDbService.test.ts
    • Added new test file for IndexedDB service constants and helpers.
  • tests/unit/utils/httpErrorCode.test.ts
    • Added new test file for HTTP error code utility.
  • tests/unit/utils/math.test.ts
    • Added new test file for mathematical utility functions.
  • tests/unit/utils/speakerUtils.test.ts
    • Added new test file for speaker utility functions.
  • tests/unit/utils/time.test.ts
    • Added new test file for time formatting and parsing utilities.
  • tests/unit/utils/videoUtils.test.ts
    • Added new test file for video utility functions.
  • vitest.config.ts
    • Added new Vitest configuration file for unit and component testing.
Ignored Files
  • Ignored by pattern: .github/workflows/** (2)
    • .github/workflows/ci.yml
    • .github/workflows/docker-publish.yml
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Reinitialize media metadata when transcription changes, not only on mount.

With prop-driven flow, mount-only initialization can leave stale audioFile, duration, and timeRange when 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 | 🟠 Major

i18n 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 | 🟠 Major

Add a runtime instance assertion for grayscale.

At Line 67 onward, grayscale is the only map block without toBeInstanceOf(RGBColor). That misses an API-contract regression currently visible in app/services/colorMapService.ts (Line 56-61), where grayscale returns 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 | 🟠 Major

Wire UFileUpload to the same file-processing path.

UFileUpload currently has no event bindings and is non-functional. Users can interact with it but no file processing occurs. The loadAudio handler is bound only to the legacy UInput element. Add either v-model="selectedFile" with a watcher or @update:modelValue="handleFileUpload" to the UFileUpload component, or bind @change="loadAudio" directly (though note that @change on UFileUpload emits a native Event, similar to UInput).

🤖 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 | 🟠 Major

These 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 | 🟠 Major

Remove the root v-if so 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 | 🟠 Major

Fix typo in transcription index key (audioFiledId).

audioFiledId appears misspelled and can break index-based queries if the stored property is audioFileId.

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 | 🟠 Major

Downgrade vue-router to 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.4 in 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 | 🟠 Major

Use 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 path transcription/${id} resolves to /transcription/transcription/:id instead 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 | 🟠 Major

Do not clear existing summary before successful regeneration.

Current flow clears transcription.summary first, 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 | 🟠 Major

Avoid 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 | 🟠 Major

Tests 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 in app/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 | 🟠 Major

Handle 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 (or transcription.mediaFile changes), stale URLs/listeners can leak and mediaDuration can 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 | 🟠 Major

Add a top-level try/catch for the media-processing pipeline.

processMedia() is started from onMounted without 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 | 🟠 Major

Use actual media MIME type in <source>.

type="video/mp4" is too narrow and can fail for valid non-MP4 video blobs. Bind mediaFile.type instead.

🤖 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 | 🟠 Major

Revoke 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 stale mediaSrc/mediaFile/isVideoFile from 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 | 🟠 Major

Reset media state when no transcription.mediaFile exists.

loadMedia() exits early without clearing mediaFile, mediaSrc, and isVideoFile, 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 | 🟠 Major

Prevent stale duration updates and blob URL leaks in initializeDuration.

initializeDuration() creates a new blob URL on every call, but revokes only in onloadedmetadata. If metadata never loads (or an older load resolves after a newer one), URLs can leak and duration can 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 | 🟠 Major

Throw an Error value, not the Ref object.

throw errorMessage throws 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 | 🟠 Major

Normalize progress values to the 0–100 scale.

calculateProgress() returns 1 for COMPLETED/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 | 🟠 Major

Parse DUMMY as 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

cleanupFailedAndCanceledTasks currently skips CANCELLED.

The filter deletes FAILED and COMPLETED, but not CANCELLED, 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 | 🟠 Major

URL parsing may extract wrong segment for task_id.

The URL pattern used in status.get.ts is /task/[r:task_id]/status, meaning the task_id is not the last segment—"status" is. Calling options.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 | 🟠 Major

Inconsistent 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:

  1. The task ID is generated once at module load time, not per request
  2. 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 dummyTaskStatusFetcher that 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 | 🟠 Major

Extract cleanupOldTranscriptions() as a standalone utility function.

Calling useTranscription() in a plugin context is problematic because the composable relies on onMounted and onUnmounted lifecycle hooks (lines 17-31 in useTranscriptions.ts) which won't fire in a plugin context. Although the cleanupOldTranscriptions() function doesn't depend on the reactive state, using composables outside components is not recommended.

Move cleanupOldTranscriptions() to a separate utility module in app/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

containerRef is declared but never bound in template.

The containerRef is declared but not assigned to any element via ref attribute in the template. The updateIndicator function 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 UButtonMotion loop:

     <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 | 🟠 Major

Memory leak: Object URL not revoked.

URL.createObjectURL allocates 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 | 🟠 Major

Use a clamped start value consistently when building update payloads.

end is currently computed from raw newStart; if newStart goes negative while start is clamped to 0, 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 | 🟠 Major

Avoid direct mutation of prop-backed segment objects.

segments now references props.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 | 🟠 Major

Reset timeline state when no media is present.

Returning early without reset can leave stale duration/timeRange from 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 | 🟠 Major

Harden update semantics: keep immutable fields immutable and always bump updatedAt.

updateTranscription currently accepts full Partial<StoredTranscription>, so callers can submit id/createdAt, and updates can land without refreshing updatedAt.

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 | 🟠 Major

Add cleanup for task polling on component unmount.

The pollTaskStatus function 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 pollTaskStatus to return a cancel function, store it in the component, and call it in onUnmounted:

// 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 | 🟠 Major

Incorrect insertion index for "after" direction.

The splice always inserts at targetIndex, but when direction === "after", the new segment should be inserted at targetIndex + 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1a1769e and 66a6e47.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (100)
  • .env.example
  • .github/workflows/ci.yml
  • .github/workflows/docker-publish.yml
  • .nuxtrc
  • AGENTS.md
  • app/app.vue
  • app/components/AudioRecordingView.client.vue
  • app/components/EditorModeSelector.vue
  • app/components/ExportToolbar.vue
  • app/components/HContainer.vue
  • app/components/KeyboardShortcutsHint.vue
  • app/components/LoadingView.vue
  • app/components/MediaEditor.vue
  • app/components/MediaPlaybackBar.vue
  • app/components/MediaPreviewView.vue
  • app/components/MediaProcessingView.vue
  • app/components/MediaProgressView.vue
  • app/components/MediaSelectionView.vue
  • app/components/NavigationMenu.vue
  • app/components/RenameSpeakerView.vue
  • app/components/SpeakerStatisticsView.vue
  • app/components/TimelineEditor.vue
  • app/components/TranscriptionEditView.vue
  • app/components/TranscriptionInfoView.vue
  • app/components/TranscriptionSummaryView.vue
  • app/components/TranscriptionViewer.vue
  • app/components/UploadMediaView.client.vue
  • app/components/media/CurrentSegementEditor.vue
  • app/components/media/TimelineView.client.vue
  • app/components/media/VideoView.vue
  • app/components/transcription/ProcessingTasksTable.vue
  • app/components/transcription/TranscriptionTable.vue
  • app/components/transcriptionList/TranscriptionList.vue
  • app/components/transcriptionList/TranscriptionSegmentEdit.vue
  • app/composables/audio_convertion.ts
  • app/composables/currentTranscription.ts
  • app/composables/export.ts
  • app/composables/transcriptionService.ts
  • app/composables/useAudioExtract.ts
  • app/composables/useAudioUpload.ts
  • app/composables/useDateFormatter.ts
  • app/composables/useSpectrogramGenerator.ts
  • app/composables/useTaskListener.ts
  • app/composables/useTaskStatus.ts
  • app/composables/useTasks.ts
  • app/composables/useTranscriptionSummary.ts
  • app/composables/useTranscriptions.ts
  • app/layouts/default.vue
  • app/pages/index.vue
  • app/pages/task/[taskId].vue
  • app/pages/transcription/[transcriptionId].vue
  • app/pages/transcription/index.vue
  • app/plugins/cleanupTranscriptions.client.ts
  • app/services/indexDbService.ts
  • app/stores/db.ts
  • app/stores/tasksStore.ts
  • app/stores/transcriptionsStore.ts
  • app/types/commands.ts
  • app/types/mediaProgress.ts
  • app/types/mediaStepInOut.ts
  • app/types/task.ts
  • app/types/transcriptionResponse.ts
  • app/utils/animationPresets.ts
  • app/utils/makrdownToDox.client.ts
  • app/utils/speakerStatistics.ts
  • app/utils/videoUtils.ts
  • biome.json
  • i18n/locales/de.json
  • i18n/locales/en.json
  • nuxt.config.ts
  • package.json
  • server/api/health/liveness.get.ts
  • server/api/health/readiness.get.ts
  • server/api/health/startup.get.ts
  • server/api/summarize/submit.post.ts
  • server/api/transcribe/[task_id]/index.get.ts
  • server/api/transcribe/[task_id]/status.get.ts
  • server/api/transcribe/submit.post.ts
  • server/assets/changelogs/v0.5.0.md
  • server/assets/changelogs/v0.6.0.md
  • server/plugins/startup-log.ts
  • server/utils/apiHanlder.ts
  • server/utils/dummyData.ts
  • server/utils/verboseFetch.ts
  • shared/types/summary.ts
  • tests/composables/dialog.test.ts
  • tests/composables/speakerColor.test.ts
  • tests/composables/useDateFormatter.test.ts
  • tests/composables/useTaskStatus.test.ts
  • tests/setup/vitest-setup.ts
  • tests/types/task.test.ts
  • tests/types/transcriptionResponse.test.ts
  • tests/unit/services/colorMapService.test.ts
  • tests/unit/services/indexDbService.test.ts
  • tests/unit/utils/httpErrorCode.test.ts
  • tests/unit/utils/math.test.ts
  • tests/unit/utils/speakerUtils.test.ts
  • tests/unit/utils/time.test.ts
  • tests/unit/utils/videoUtils.test.ts
  • vitest.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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Missing language entry: kn (Kannada) is present in de.json but 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 | 🟡 Minor

Incorrect language mapping: ba should be "Bashkir", not "Basque".

The ISO 639-1 code ba corresponds to Bashkir (a Turkic language), not Basque. Basque is already correctly mapped to eu on 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 | 🟡 Minor

Update 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 for TaskNotFound.

The key TaskNotFound uses PascalCase while all other error keys in this section (and throughout the file) use camelCase (e.g., noMediaFile, failedToLoad, noResult). Consider renaming to taskNotFound for consistency.

🔧 Suggested fix
-            "TaskNotFound": "Aufgabe nicht gefunden"
+            "taskNotFound": "Aufgabe nicht gefunden"

Note: This change would also need to be applied to en.json and 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 for TaskNotFound.

Same issue as in de.json - this key uses PascalCase while all other error keys use camelCase. Consider renaming to taskNotFound for 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: Redundant loadMedia() call on mount.

Both onMounted (line 40) and the watcher with { immediate: true } (line 62) call loadMedia() on initial mount, causing double execution. The watcher alone is sufficient since immediate: true triggers 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 shared downloadBlob helper 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: Rename transciptiontranscription for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 067b669 and d086146.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • app/components/EditorModeSelector.vue
  • app/components/ExportToolbar.vue
  • app/components/MediaPlaybackBar.vue
  • app/components/MediaProcessingView.vue
  • app/components/UploadMediaView.client.vue
  • app/composables/export.ts
  • app/composables/useAudioUpload.ts
  • i18n/locales/de.json
  • i18n/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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 — useI18n is 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 potential play() rejection.

The play() method returns a Promise that can reject (e.g., due to browser autoplay policies). Currently, isPlaying is 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 using computed for i18n-reactive options.

The audioLanguageOptions uses ref() with t() calls at setup time. If the locale changes while this component is mounted, the labels won't update. Using computed would 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 speakerOptions on 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a17981 and b9f0703.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • app/components/HContainer.vue
  • app/components/MediaPlaybackBar.vue
  • app/components/MediaPreviewView.vue
  • app/components/NavigationMenu.vue
  • app/components/TranscriptionInfoView.vue
  • app/components/media/VideoView.vue
  • package.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

Tobias Bollinger added 10 commits March 2, 2026 14:02
- 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
@swordbreaker swordbreaker merged commit 84c8eaf into main Mar 3, 2026
2 checks passed
@swordbreaker swordbreaker deleted the feature/ui-redesing branch March 3, 2026 13:23
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.

1 participant