Skip to content

Conversation

@Metal079
Copy link
Owner

@Metal079 Metal079 commented Jan 9, 2026

Motivation

  • Improve the LoRAs panel usability by adding richer filtering and sorting options (last used, favorites, alphabetical) to make selection faster and clearer on desktop and mobile.
  • Allow users to mark frequently used LoRAs as favorites and persist usage/favorite state across sessions for better discovery.
  • Provide a compact, responsive UI that groups search, sort and favorites controls into a single toolbar and simplifies the LoRA cards layout.

Description

  • Added persisted LoRA preferences: favorites and last-used timestamps stored under loraFavorites and loraLastUsed in localStorage, with helper methods loadLoraPreferences, persistLoraFavorites, and persistLoraLastUsed in options.component.ts.
  • Introduced new sorting and filtering options (loraSortOrder, loraFavoritesOnly) and integrated them into filterLoras, supporting most_used, last_used, favorites, alpha_asc, and alpha_desc modes and favorites-only filtering.
  • Mark LoRAs as last-used when selected or when loading history via markLoraLastUsed, and added per-card favorite toggles with isLoraFavorite/toggleLoraFavorite and UI wiring in the template.
  • Reworked the LoRAs UI: added a toolbar with search/sort/favorites controls, per-card favorite button, and updated CSS for a responsive grid and improved card layout (changes in options.component.html and options.component.css).

Testing

  • Performed a development build and served the app using ng serve, and the build completed successfully.
  • Launched a simple Playwright script to open the app and capture a screenshot of the LoRAs panel, which produced artifacts/loras-tab.png showing the new UI.
  • No unit tests were added or run as part of this change.

Codex Task

Copilot AI review requested due to automatic review settings January 9, 2026 02:03
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This 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.

Comment on lines +2637 to +2651
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 || '');
}
});
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
if (versionId != null) return String(versionId);
const name = lora?.name ?? '';
const version = lora?.version ?? '';
return `${name}::${version}`.toLowerCase();
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
return `${name}::${version}`.toLowerCase();
return JSON.stringify({ name, version }).toLowerCase();

Copilot uses AI. Check for mistakes.
Comment on lines +2515 to +2593
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;
}
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +2529 to +2540
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 = {};
}
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
/* Set a minimum height to prevent layout shifts */
max-width: 600px;
/* Enforce a consistent card width */
overflow: hidden;
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
overflow: hidden;
overflow: visible;

Copilot uses AI. Check for mistakes.
Comment on lines +583 to +596
<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>
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
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)">
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
<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'">

Copilot uses AI. Check for mistakes.
Comment on lines +601 to +603
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly"
(ngModelChange)="filterLoras()">
<span class="form-check-label">Show</span>
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
if (versionId != null) return String(versionId);
const name = lora?.name ?? '';
const version = lora?.version ?? '';
return `${name}::${version}`.toLowerCase();
Copy link

Copilot AI Jan 9, 2026

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.

Suggested change
return `${name}::${version}`.toLowerCase();
return `${name}::${version}`;

Copilot uses AI. Check for mistakes.
Comment on lines +2543 to +2557
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);
}
}
Copy link

Copilot AI Jan 9, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants