Skip to content

feat(ui): add responsive search & filter to Voice Library and Main Editor (fixes #966) - #1016

Open
devangkantharia wants to merge 4 commits into
jamiepine:mainfrom
devangkantharia:feat/voice-library-search-966
Open

feat(ui): add responsive search & filter to Voice Library and Main Editor (fixes #966)#1016
devangkantharia wants to merge 4 commits into
jamiepine:mainfrom
devangkantharia:feat/voice-library-search-966

Conversation

@devangkantharia

@devangkantharia devangkantharia commented Aug 9, 2026

Copy link
Copy Markdown

Summary

This PR implements search and filtering capabilities across the Generate Tab (MainEditor.tsx) and Voice Library (VoicesTab.tsx) to solve #966.

Users can now instantly search profiles by text query or filter them by voice type and language instead of manually scrolling through the list.


What Was Added

  1. Live Search Bar (MainEditor.tsx & VoicesTab.tsx):

    • Placed below the main title row matching the search bar design of Stories and Captures tabs.
    • Filters profiles in real time by voice name, description, language, and engine.
    • Includes an instant X clear search button.
  2. Voice Type & Language Filters (VoicesTab.tsx):

    • Voice Type Filter: Isolate profiles by Preset (built-in models), Cloned (voice reference samples), or Designed (character prompts).
    • Language Filter: Filter profiles by language (English, Spanish, French, German, Hindi, Japanese, etc.).
    • Includes a Clear Filters reset button.
  3. Empty States & i18n Localization:

    • Added user-friendly empty state views when search returns no matching profiles.
    • Added translation keys for search placeholders, filter options, and empty states in translation.json.

Changed Files

  • app/src/components/MainEditor/MainEditor.tsx – Added search bar and search state under the main header row.
  • app/src/components/VoiceProfiles/ProfileList.tsx – Added search filtering and empty search state card.
  • app/src/components/VoicesTab/VoicesTab.tsx – Redesigned header toolbar with search bar, Voice Type filter, Language filter, and reset button.
  • app/src/i18n/locales/en/translation.json – Added i18n keys for search & filter UI text.
  • app/src/components/Generation/FloatingGenerateBox.tsx & languages.ts – Fixed engine auto-switching sequence for profile selection and language code mapping.

image

Summary by CodeRabbit

  • New Features
    • Search voice profiles by name, description, language, voice type, or engine.
    • Filter profiles with clearer controls, dynamic language options, and no-results messaging.
    • Import voice profiles from validated .voicebox.zip files with confirmation and success/error feedback.
    • Profiles are organized with supported and selected voices prioritized.
  • Bug Fixes
    • Language selections now stay compatible with the chosen engine.
  • Style
    • Improved responsive layouts, profile selection states, and hover scrollbar behavior.
    • Added localized text for new search, filtering, and import experiences.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee5c9136-4f5f-4182-bd69-4a40bfd8d26f

📥 Commits

Reviewing files that changed from the base of the PR and between ead8112 and 2bbf990.

📒 Files selected for processing (2)
  • app/src/components/MainEditor/MainEditor.tsx
  • app/src/components/VoicesTab/VoicesTab.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/components/VoicesTab/VoicesTab.tsx
  • app/src/components/MainEditor/MainEditor.tsx

📝 Walkthrough

Walkthrough

The PR adds searchable and filterable voice-profile lists, .voicebox.zip import support, responsive layout updates, profile-card styling, hover scrollbar utilities, and engine-aware language synchronization.

Changes

Voice profile experience

Layer / File(s) Summary
Profile synchronization and language support
app/src/components/Generation/FloatingGenerateBox.tsx
Engine selection and fallback handling now run before profile language synchronization.
Main editor profile search and presentation
app/src/components/MainEditor/MainEditor.tsx, app/src/components/VoiceProfiles/ProfileList.tsx, app/src/components/VoiceProfiles/ProfileCard.tsx, app/src/components/History/HistoryTable.tsx, app/src/index.css, app/src/i18n/locales/en/translation.json
The editor passes controlled search state to ProfileList. Profiles are filtered across profile and engine metadata, sorted by support and selection state, and displayed in a responsive grid. Cards use border-based states. Search empty states and hover scrollbar styling are included.
Voices tab import and filtering
app/src/components/VoicesTab/VoicesTab.tsx, app/src/i18n/locales/en/translation.json
The Voices tab validates .voicebox.zip files, confirms imports, reports mutation results, and filters profiles by type, language, name, description, and engine.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant VoicesTab
  participant useImportProfile
  participant Toast
  User->>VoicesTab: Select .voicebox.zip file
  VoicesTab->>VoicesTab: Validate file and request confirmation
  User->>VoicesTab: Confirm import
  VoicesTab->>useImportProfile: Import profile
  useImportProfile-->>VoicesTab: Return success or error
  VoicesTab->>Toast: Show import result
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: responsive search and filtering in the Voice Library and Main Editor.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/MainEditor/MainEditor.tsx`:
- Around line 117-124: Add the localized clear-search accessible name
aria-label={t('main.clearSearch')} to the button in MainEditor.tsx at lines
117-124 and the corresponding button in VoicesTab.tsx at lines 236-243,
preserving their existing behavior and styling.

In `@app/src/components/VoiceProfiles/ProfileList.tsx`:
- Around line 120-122: Update the query-specific empty-state paragraph in the
ProfileList component to use the existing t() translation function with an
interpolated search query. Add or reuse a translation key containing the search
placeholder, and preserve the current message and styling for locales with the
translated text.

In `@app/src/components/VoicesTab/VoicesTab.tsx`:
- Around line 72-74: Update the search matching logic in VoicesTab to assign q
from search.trim() before lowercasing, so surrounding whitespace is excluded
from matching while preserving the existing case-insensitive name search.
- Around line 352-369: Use a shared dialog-close handler for the Dialog’s
onOpenChange and the cancel button so every close path sets importDialogOpen to
false, clears selectedFile, and resets fileInputRef.current.value. Replace the
inline cancel cleanup while preserving the existing import dialog behavior.

In `@app/src/lib/constants/languages.ts`:
- Line 42: Remove Hindi (`hi`) from the `qwen`, `qwen_custom_voice`, and `tada`
language maps in the language constants, while preserving it for backends that
explicitly support Hindi.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09b8bb20-72cc-422d-bf64-46559ef11ece

📥 Commits

Reviewing files that changed from the base of the PR and between 51f49de and ab6fc04.

📒 Files selected for processing (9)
  • app/src/components/Generation/FloatingGenerateBox.tsx
  • app/src/components/History/HistoryTable.tsx
  • app/src/components/MainEditor/MainEditor.tsx
  • app/src/components/VoiceProfiles/ProfileCard.tsx
  • app/src/components/VoiceProfiles/ProfileList.tsx
  • app/src/components/VoicesTab/VoicesTab.tsx
  • app/src/i18n/locales/en/translation.json
  • app/src/index.css
  • app/src/lib/constants/languages.ts

Comment thread app/src/components/MainEditor/MainEditor.tsx
Comment thread app/src/components/VoiceProfiles/ProfileList.tsx
Comment thread app/src/components/VoicesTab/VoicesTab.tsx
Comment thread app/src/components/VoicesTab/VoicesTab.tsx Outdated
Comment thread app/src/lib/constants/languages.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/components/VoicesTab/VoicesTab.tsx (1)

102-109: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the file input after rejecting an invalid file.

At Line 108, the handler returns without clearing the input value. If the user selects the same invalid file again, the browser may not emit another change event, so the user receives no new feedback. Clear e.target.value before returning.

Proposed fix
       if (!file.name.endsWith('.voicebox.zip')) {
+        e.target.value = '';
         toast({
           title: t('main.import.invalidTitle'),
           description: t('main.import.invalidDescription'),
           variant: 'destructive',
         });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/VoicesTab/VoicesTab.tsx` around lines 102 - 109, In the
invalid-file branch of the file change handler in VoicesTab, clear the file
input value via e.target.value before returning after the destructive toast, so
selecting the same invalid file again triggers a new change event.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@app/src/components/VoicesTab/VoicesTab.tsx`:
- Around line 102-109: In the invalid-file branch of the file change handler in
VoicesTab, clear the file input value via e.target.value before returning after
the destructive toast, so selecting the same invalid file again triggers a new
change event.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 194f3adc-5049-4d1c-9958-25b1d973a7b5

📥 Commits

Reviewing files that changed from the base of the PR and between ab6fc04 and ead8112.

📒 Files selected for processing (4)
  • app/src/components/MainEditor/MainEditor.tsx
  • app/src/components/VoiceProfiles/ProfileList.tsx
  • app/src/components/VoicesTab/VoicesTab.tsx
  • app/src/i18n/locales/en/translation.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/i18n/locales/en/translation.json
  • app/src/components/MainEditor/MainEditor.tsx
  • app/src/components/VoiceProfiles/ProfileList.tsx

@Lvigentini

Copy link
Copy Markdown

Heads-up on a file overlap rather than a review comment — no objection to this PR, the two changes are complementary.

#1005 (folders + list view for voices and clips) touches three of the same files:

  • app/src/components/VoiceProfiles/ProfileList.tsx — +246 lines there, folder sections and a card/list toggle
  • app/src/components/VoiceProfiles/ProfileCard.tsx
  • app/src/components/History/HistoryTable.tsx

Search and folders want to compose — filter within a folder is the natural end state — but whichever of these lands second will need a rebase through those three files. Flagging so @jamiepine can sequence rather than discover it at merge time. If this one goes first I'll take the rebase on my side.

One clarification for #966 while I'm here: the Voices tab already has a search box on main (VoicesTab.tsx), so the gap that issue is really pointing at is the Generate-tab picker in the screenshot — which is what the Main Editor half of this PR covers.

Lvigentini added a commit to Lvigentini/voicebox that referenced this pull request Aug 9, 2026
…mposes

jamiepine#1016 adds voice search and threads a query into ProfileList as a prop. This
branch rewrites the same component for folders and a card/list toggle, so the
two collide in exactly one file. Rather than leave that to whoever rebases
second, ProfileList now takes the same `search` prop and does the filtering
folder-aware.

Filtering happens before bucketing, which is the part that only makes sense
once both features exist: a match stays in the folder it belongs to instead of
collapsing into one flat list. Folders with no matches are hidden while a query
is active -- a wall of empty folder headers is noise -- but they come back when
the query clears, since the header is also the drop target for filing a voice.

The match predicate is jamiepine#1016's (name, description, language, preset_engine,
default_engine), and so is the alphabetical tiebreak in the sort; without it the
order inside a folder is whatever the API returned. Their selected-profile-first
rule is deliberately not adopted -- it reads differently once voices are grouped,
and it interacts with the scroll-into-view effect above, so that is a UX call
for the author of jamiepine#1016 and the maintainer, not one to make silently here.

`profiles.list.noVoicesMatch` is added in all nine locales rather than English
only, so the string is not left half-translated.

No search box is added here. The input, the VoicesTab filters and the layout
work stay in jamiepine#1016; this is only the seam it plugs into.

Refs jamiepine#1016, jamiepine#966

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Lvigentini

Copy link
Copy Markdown

Following up on my earlier comment — I went and read both diffs properly rather than leaving it at "these will conflict", and then did something about it. Summary: the real overlap is one file, and I've made my side of it accept your search prop, so this PR should end up smaller rather than fighting mine.

The overlap is narrower than the file list suggests

Of the three shared files, two are non-issues:

  • HistoryTable.tsx — you add hover-scrollbar to one className. My branch changes a different region (folder tree + clip search). Trivial either way.
  • ProfileCard.tsx — you restyle the selected/disabled states (ring-2border-2). I add an onPreview prop and a preview button. Different hunks; likely auto-merges.

ProfileList.tsx is the genuine collision. You restructure it around a filtered flat list; I restructure it around folder buckets and a card/list toggle. Same component, incompatible shapes, no auto-merge.

What I changed on my branch

3addcb1 makes ProfileList take the same search / onClearSearch props you defined, and does the filtering before folder bucketing — so a match stays in its folder instead of collapsing into a flat list. Folders with no matches hide while a query is active and come back when it clears, since the header is also the drop target for filing a voice.

I used your match predicate as-is (name, description, language, preset_engine, default_engine) and your alphabetical sort tiebreak — that one's a straight improvement over what I had, which left intra-folder order to whatever the API returned. Also added profiles.list.noVoicesMatch across all nine locales rather than en only, so that string isn't left half-translated.

No search box on my side. The input, the VoicesTab filters, FloatingGenerateBox, index.css — all still yours. I only built the seam.

If #1005 lands first, your ProfileList.tsx hunk should drop out entirely and the rest applies unchanged.

Two things I deliberately did not decide

Selected-profile-first sorting. Your sort is supported → selected → alphabetical. I took the alphabetical part but not selected-first: with voices grouped into folders it reads differently, and it interacts with the scroll-into-view effect directly above it (selecting reorders, then the effect scrolls to the moved card). Might be fine, might be jumpy — that's yours and @jamiepine's call, and easy to add on top of what I've pushed.

The layout rework — @jamiepine, this one needs you. This PR isn't only search. It also changes MainEditor to fixed-viewport panes (h-[40vh] / h-[38vh]), adds a sticky blurred search header with a pt-14pt-24 offset, and swaps the profile grid from horizontal-scroll-on-mobile to grid-cols-2 md:grid-cols-3. My branch leaves main's card layout alone and adds an opt-in list view instead.

I'm not claiming mine is better — the horizontal scroll on narrow viewports is genuinely awkward and this may well be the right fix. But it's a product decision bundled into a search PR, it's the one part that can't be mechanically composed with folder sections, and whichever of us defers should do it deliberately rather than at rebase time. Splitting the layout change into its own PR would make both of these trivial to land.

Happy to take the rebase on my side either way — just say which order you want.

@devangkantharia

Copy link
Copy Markdown
Author

Thanks @Lvigentini for the thoughtful review and for proactively updating your branch 3addcb1 to integrate the search props! Really appreciate the great collaboration. 👍

To share a bit of context on the layout adjustments in MainEditor:

When testing the search feature on smaller window sizes and split-screen views, I noticed that single-row horizontal scrolling caused cards to clip and obscured the history table behind the floating input bar. To make searching and browsing voices feel smooth on all screen sizes, I added a few responsive polish details alongside the search bar:

  1. Responsive Grid (grid-cols-2 md:grid-cols-3): Allows voice cards to wrap naturally into 2–3 columns so titles and tags aren't truncated horizontally on narrower screens.
  2. Sticky Blurred Search Header: Keeps the Voicebox title, search input, and creation buttons pinned at the top (backdrop-blur-sm) while scrolling through profiles, so search controls remain instantly accessible at all times.
  3. Viewport Panes (40vh / 38vh): Caps the profile grid and history section on small viewports with hover scrollbars (.hover-scrollbar), ensuring both sections stay visible simultaneously without covering the floating generate bar.

I felt these layout details made the search experience feel more cohesive on smaller screens, but I am completely open to @jamiepine's guidance on whether to keep them together or split/sequence them alongside PR #1005! 🙌

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.

2 participants