-
Notifications
You must be signed in to change notification settings - Fork 0
LoRA tab: add favorites, sorting, and responsive toolbar #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request enhances the LoRAs panel by adding favorites functionality, multiple sorting options (most used, last used, favorites first, alphabetical), and a responsive toolbar UI. User preferences for favorites and last-used timestamps are persisted in localStorage for better user experience across sessions.
Key changes:
- Added localStorage-backed favorites and last-used tracking with helper methods for persistence and retrieval
- Implemented comprehensive sorting and filtering options integrated into a new responsive toolbar
- Introduced per-card favorite toggle buttons with visual indicators using Bootstrap icons
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
| src/app/home/options/options.component.ts | Added LoRA preferences (favorites, last-used), localStorage persistence methods, updated filterLoras with new sorting logic, and integrated markLoraLastUsed tracking |
| src/app/home/options/options.component.html | Replaced search bar with responsive toolbar containing search, sort dropdown, and favorites toggle; added favorite button to each LoRA card |
| src/app/home/options/options.component.css | Converted LoRAs grid to CSS Grid layout, added toolbar styling with responsive breakpoints, and styled the favorite button overlay |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| this.filteredLoras = [...this.filteredLoras].sort((a, b) => { | ||
| switch (this.loraSortOrder) { | ||
| case 'last_used': | ||
| return this.getLoraLastUsed(b) - this.getLoraLastUsed(a) || (a?.name || '').localeCompare(b?.name || ''); | ||
| case 'alpha_asc': | ||
| return (a?.name || '').localeCompare(b?.name || ''); | ||
| case 'alpha_desc': | ||
| return (b?.name || '').localeCompare(a?.name || ''); | ||
| case 'favorites': | ||
| return Number(this.isLoraFavorite(b)) - Number(this.isLoraFavorite(a)) || (a?.name || '').localeCompare(b?.name || ''); | ||
| case 'most_used': | ||
| default: | ||
| return (b?.uses || 0) - (a?.uses || 0) || (a?.name || '').localeCompare(b?.name || ''); | ||
| } | ||
| }); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sorting logic is applied after the favorites-only filter. This means when loraFavoritesOnly is true and loraSortOrder is 'favorites', the sort will still execute but won't be meaningful since all items are already favorites. Consider optimizing by skipping the 'favorites' sort when loraFavoritesOnly is active, or applying the favorites filter after sorting to maintain better separation of concerns.
| if (versionId != null) return String(versionId); | ||
| const name = lora?.name ?? ''; | ||
| const version = lora?.version ?? ''; | ||
| return `${name}::${version}`.toLowerCase(); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getLoraKey method constructs a fallback key using name and version with "::" as separator, but this could create collisions if a LoRA name or version contains "::". Consider using a safer delimiter (e.g., "\0" or JSON.stringify) or validating that the separator doesn't appear in the values to ensure unique keys for different LoRAs.
| return `${name}::${version}`.toLowerCase(); | |
| return JSON.stringify({ name, version }).toLowerCase(); |
| private loadLoraPreferences() { | ||
| try { | ||
| const rawFavorites = localStorage.getItem(this.loraFavoritesKey); | ||
| if (rawFavorites) { | ||
| const parsed = JSON.parse(rawFavorites); | ||
| if (Array.isArray(parsed)) { | ||
| this.loraFavorites = new Set(parsed.filter((id) => typeof id === 'string')); | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.warn('Failed to load LoRA favorites:', error); | ||
| this.loraFavorites = new Set(); | ||
| } | ||
|
|
||
| try { | ||
| const rawLastUsed = localStorage.getItem(this.loraLastUsedKey); | ||
| if (rawLastUsed) { | ||
| const parsed = JSON.parse(rawLastUsed); | ||
| if (parsed && typeof parsed === 'object') { | ||
| this.loraLastUsed = parsed; | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.warn('Failed to load LoRA last used history:', error); | ||
| this.loraLastUsed = {}; | ||
| } | ||
| } | ||
|
|
||
| private persistLoraFavorites() { | ||
| try { | ||
| localStorage.setItem(this.loraFavoritesKey, JSON.stringify([...this.loraFavorites])); | ||
| } catch (error) { | ||
| console.warn('Failed to save LoRA favorites:', error); | ||
| } | ||
| } | ||
|
|
||
| private persistLoraLastUsed() { | ||
| try { | ||
| localStorage.setItem(this.loraLastUsedKey, JSON.stringify(this.loraLastUsed)); | ||
| } catch (error) { | ||
| console.warn('Failed to save LoRA last used history:', error); | ||
| } | ||
| } | ||
|
|
||
| private getLoraKey(lora: any): string { | ||
| const versionId = lora?.version_id ?? lora?.versionId ?? lora?.id; | ||
| if (versionId != null) return String(versionId); | ||
| const name = lora?.name ?? ''; | ||
| const version = lora?.version ?? ''; | ||
| return `${name}::${version}`.toLowerCase(); | ||
| } | ||
|
|
||
| isLoraFavorite(lora: any): boolean { | ||
| return this.loraFavorites.has(this.getLoraKey(lora)); | ||
| } | ||
|
|
||
| toggleLoraFavorite(lora: any, event?: Event) { | ||
| if (event) { | ||
| event.stopPropagation(); | ||
| } | ||
| const key = this.getLoraKey(lora); | ||
| if (this.loraFavorites.has(key)) { | ||
| this.loraFavorites.delete(key); | ||
| } else { | ||
| this.loraFavorites.add(key); | ||
| } | ||
| this.persistLoraFavorites(); | ||
| this.filterLoras(); | ||
| } | ||
|
|
||
| private markLoraLastUsed(lora: any) { | ||
| const key = this.getLoraKey(lora); | ||
| this.loraLastUsed[key] = Date.now(); | ||
| this.persistLoraLastUsed(); | ||
| } | ||
|
|
||
| private getLoraLastUsed(lora: any): number { | ||
| return this.loraLastUsed[this.getLoraKey(lora)] ?? 0; | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feature adds significant new functionality (favorites, last-used tracking, multiple sort modes) but no test coverage is provided. Since the component already has a spec file (options.component.spec.ts), consider adding tests for the new methods: loadLoraPreferences, persistLoraFavorites, persistLoraLastUsed, getLoraKey, isLoraFavorite, toggleLoraFavorite, markLoraLastUsed, and the updated filterLoras sorting logic.
| try { | ||
| const rawLastUsed = localStorage.getItem(this.loraLastUsedKey); | ||
| if (rawLastUsed) { | ||
| const parsed = JSON.parse(rawLastUsed); | ||
| if (parsed && typeof parsed === 'object') { | ||
| this.loraLastUsed = parsed; | ||
| } | ||
| } | ||
| } catch (error) { | ||
| console.warn('Failed to load LoRA last used history:', error); | ||
| this.loraLastUsed = {}; | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loadLoraPreferences method validates that parsed favorites is an array and filters for strings, but doesn't validate the loraLastUsed object structure. Consider adding validation to ensure that parsed values in loraLastUsed are numbers (timestamps) to prevent invalid data from corrupting the last-used tracking.
| /* Set a minimum height to prevent layout shifts */ | ||
| max-width: 600px; | ||
| /* Enforce a consistent card width */ | ||
| overflow: hidden; |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lora-card overflow property changed from 'visible' to 'hidden'. If any child elements (like tooltips or dropdowns) need to overflow the card boundaries, they will now be clipped. Verify that all interactive elements within lora-card work correctly with overflow: hidden, or consider using overflow: visible for specific scenarios if needed.
| overflow: hidden; | |
| overflow: visible; |
| <label class="form-label">Search LoRAs</label> | ||
| <input type="text" class="form-control" placeholder="Search by name or version..." [(ngModel)]="loraSearchQuery" | ||
| (ngModelChange)="filterLoras()"> | ||
| </div> | ||
| <div class="lora-toolbar__controls"> | ||
| <div class="lora-toolbar__control"> | ||
| <label class="form-label">Sort by</label> | ||
| <select class="form-select" [(ngModel)]="loraSortOrder" (ngModelChange)="filterLoras()"> | ||
| <option value="most_used">Most used</option> | ||
| <option value="last_used">Last used</option> | ||
| <option value="favorites">Favorites first</option> | ||
| <option value="alpha_asc">Alphabetical A → Z</option> | ||
| <option value="alpha_desc">Alphabetical Z → A</option> | ||
| </select> |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The label elements for "Search LoRAs" and "Sort by" are not properly associated with their inputs. Add a for attribute to the label pointing to a unique id on the input/select element, or wrap the input/select within the label element. This improves accessibility for screen readers and allows clicking the label to focus the input.
| Request LoRAs! | ||
| </button> | ||
| <div *ngFor="let lora of filteredLoras" class="lora-card"> | ||
| <button class="btn btn-light lora-favorite-btn" type="button" (click)="toggleLoraFavorite(lora, $event)"> |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The favorite button uses an icon without any accessible text label. Screen reader users won't know what this button does. Add an aria-label attribute (e.g., aria-label="Toggle favorite") or use a visually-hidden text element to provide context for assistive technologies.
| <button class="btn btn-light lora-favorite-btn" type="button" (click)="toggleLoraFavorite(lora, $event)"> | |
| <button | |
| class="btn btn-light lora-favorite-btn" | |
| type="button" | |
| (click)="toggleLoraFavorite(lora, $event)" | |
| [attr.aria-label]="isLoraFavorite(lora) ? 'Remove from favorites' : 'Add to favorites'"> |
| <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly" | ||
| (ngModelChange)="filterLoras()"> | ||
| <span class="form-check-label">Show</span> |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The switch input uses role="switch" which is correct, but the associated label "Show" is not properly linked to the input via a for attribute or by wrapping both elements. This can cause accessibility issues. Either wrap the input and label in the label element, or add an id to the input and use for attribute on the label to ensure proper association.
| <input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly" | |
| (ngModelChange)="filterLoras()"> | |
| <span class="form-check-label">Show</span> | |
| <input | |
| id="loraFavoritesOnlySwitch" | |
| class="form-check-input" | |
| type="checkbox" | |
| role="switch" | |
| [(ngModel)]="loraFavoritesOnly" | |
| (ngModelChange)="filterLoras()"> | |
| <label class="form-check-label" for="loraFavoritesOnlySwitch">Show</label> |
| if (versionId != null) return String(versionId); | ||
| const name = lora?.name ?? ''; | ||
| const version = lora?.version ?? ''; | ||
| return `${name}::${version}`.toLowerCase(); |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getLoraKey method lowercases the name::version fallback key, but the resolveHistoryLoras method uses name::version keys without lowercasing when building the byNameVersion Map. This inconsistency could prevent proper resolution of LoRAs that don't have version_id when restoring from history, potentially causing favorites and last-used data to not match correctly. Ensure consistent casing is used in both methods.
| return `${name}::${version}`.toLowerCase(); | |
| return `${name}::${version}`; |
| private persistLoraFavorites() { | ||
| try { | ||
| localStorage.setItem(this.loraFavoritesKey, JSON.stringify([...this.loraFavorites])); | ||
| } catch (error) { | ||
| console.warn('Failed to save LoRA favorites:', error); | ||
| } | ||
| } | ||
|
|
||
| private persistLoraLastUsed() { | ||
| try { | ||
| localStorage.setItem(this.loraLastUsedKey, JSON.stringify(this.loraLastUsed)); | ||
| } catch (error) { | ||
| console.warn('Failed to save LoRA last used history:', error); | ||
| } | ||
| } |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The localStorage operations (setItem) can throw QuotaExceededError if storage is full, but the error handling only logs a warning. Consider implementing a fallback strategy or notifying the user when localStorage operations fail, especially for persistLoraFavorites and persistLoraLastUsed, so users understand why their preferences aren't being saved.
Motivation
Description
loraFavoritesandloraLastUsedinlocalStorage, with helper methodsloadLoraPreferences,persistLoraFavorites, andpersistLoraLastUsedinoptions.component.ts.loraSortOrder,loraFavoritesOnly) and integrated them intofilterLoras, supportingmost_used,last_used,favorites,alpha_asc, andalpha_descmodes and favorites-only filtering.markLoraLastUsed, and added per-card favorite toggles withisLoraFavorite/toggleLoraFavoriteand UI wiring in the template.options.component.htmlandoptions.component.css).Testing
ng serve, and the build completed successfully.artifacts/loras-tab.pngshowing the new UI.Codex Task