Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 61 additions & 14 deletions src/app/home/options/options.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -179,25 +179,49 @@
flex-wrap: wrap;
}

.loras-grid {
.lora-toolbar {
display: grid;
gap: 12px;
align-items: end;
}

.lora-toolbar__controls {
display: grid;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}

.lora-toolbar__control .form-label {
margin-bottom: 0.25rem;
}

.lora-toolbar__toggle .form-check {
display: flex;
flex-wrap: wrap;
gap: 15px;
/* Space between items */
align-items: center;
gap: 0.5rem;
}

.lora-toolbar__toggle .form-check-label {
margin: 0;
}

.loras-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
gap: 16px;
overflow-y: auto;
/* Enable scrolling if the content exceeds the height */
overflow-x: hidden;
/* Hide horizontal scrollbar */
}

.lora-card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: visible;
min-height: 190px;
/* 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.
min-height: 230px;
background: #fff;
position: relative;
display: flex;
flex-direction: column;
}

.lora-image {
Expand All @@ -208,6 +232,21 @@
/* Align the image towards the top */
}

.lora-favorite-btn {
position: absolute;
top: 8px;
right: 8px;
border-radius: 999px;
padding: 0.25rem 0.5rem;
border: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
z-index: 2;
}

.lora-favorite-btn i {
color: #f5b700;
}

.lora-info {
padding: 0.5rem;
padding-bottom: 0;
Expand Down Expand Up @@ -240,7 +279,7 @@
align-items: center;
margin-bottom: 10px;
/* flex-wrap: nowrap; Prevent wrapping */
max-width: 30%;
max-width: 100%;
/* Ensure it doesn't exceed the container width */
}

Expand Down Expand Up @@ -346,16 +385,20 @@
}

.lora-card {
width: calc(33% - 15px);
border: 1px solid #ddd;
width: 100%;
box-sizing: border-box;
}

.btn.btn-primary.lora-request-btn {
width: 100%;
grid-column: 1 / -1;
}

@media (max-width: 768px) {
.lora-toolbar__controls {
grid-template-columns: 1fr;
}

.button-group {
flex-direction: row;
width: 100%;
Expand All @@ -375,6 +418,10 @@
}

@media (max-width: 480px) {
.loras-grid {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

.input-button-container {
flex-direction: column;
align-items: flex-start;
Expand Down
34 changes: 30 additions & 4 deletions src/app/home/options/options.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -577,10 +577,33 @@ <h5>Load LoRAs?</h5>
[style]="{ width: '100%', marginBottom: '10px' }" (onChange)="filterLoras()">
</p-multiSelect>

<!-- Search and Add Custom LoRA -->
<div class="d-flex justify-content-between mb-3">
<input type="text" class="form-control w-80" placeholder="Search LoRAs..." [(ngModel)]="loraSearchQuery"
(ngModelChange)="filterLoras()">
<!-- Search + Sort + Favorites -->
<div class="lora-toolbar mb-3">
<div class="lora-toolbar__search">
<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>
Comment on lines +583 to +596
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.
</div>
<div class="lora-toolbar__control lora-toolbar__toggle">
<label class="form-label">Favorites only</label>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="loraFavoritesOnly"
(ngModelChange)="filterLoras()">
<span class="form-check-label">Show</span>
Comment on lines +601 to +603
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.
</div>
</div>
</div>
</div>

<!-- Full-Sized Image Dialog -->
Expand All @@ -599,6 +622,9 @@ <h5>Full Lora Preview</h5>
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.
<i class="bi" [ngClass]="isLoraFavorite(lora) ? 'bi-star-fill' : 'bi-star'"></i>
</button>
<img [src]="lora.image_url" [alt]="lora.name" class="lora-image"
(click)="openImageModalLoraPreview(lora.image_url)">
<div class="lora-info">
Expand Down
120 changes: 115 additions & 5 deletions src/app/home/options/options.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ export class OptionsComponent implements OnInit {
filteredLoras: any[] = [];
selectedLoras: any[] = [];
maxLoras: number = 3;
loraSortOrder: 'most_used' | 'last_used' | 'alpha_asc' | 'alpha_desc' | 'favorites' = 'most_used';
loraFavoritesOnly: boolean = false;
private loraFavorites = new Set<string>();
private loraLastUsed: Record<string, number> = {};
private readonly loraFavoritesKey = 'loraFavorites';
private readonly loraLastUsedKey = 'loraLastUsed';

// to show full sized lora image
displayModal: boolean = false;
Expand Down Expand Up @@ -586,6 +592,8 @@ export class OptionsComponent implements OnInit {
this.generationRequest.prompt = value;
});

this.loadLoraPreferences();

// Subscribe to sync status changes
this.imageSyncService.syncStatus$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(status => {
this.syncStatus = status;
Expand Down Expand Up @@ -1923,6 +1931,7 @@ export class OptionsComponent implements OnInit {
return;
}
this.selectedLoras = this.resolveHistoryLoras(image.loras);
this.selectedLoras.forEach((lora) => this.markLoraLastUsed(lora));
this.generationRequest.loras = this.selectedLoras;
this.updateCreditCost();
}
Expand Down Expand Up @@ -2503,6 +2512,86 @@ export class OptionsComponent implements OnInit {
});
}

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 = {};
}
Comment on lines +2529 to +2540
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.
}

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);
}
}
Comment on lines +2543 to +2557
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.

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();
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.
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.
}

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;
}
Comment on lines +2515 to +2593
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.


loadLoras() {
this.stableDiffusionService.getLoras().pipe(
Expand All @@ -2529,17 +2618,37 @@ export class OptionsComponent implements OnInit {
}

// Then filter by search query
this.filteredLoras = this.filteredLoras.filter(lora =>
(lora.name.toLowerCase().includes(this.loraSearchQuery.toLowerCase()) || lora.version.toLowerCase().includes(this.loraSearchQuery.toLowerCase()))
);
const query = this.loraSearchQuery.toLowerCase();
this.filteredLoras = this.filteredLoras.filter(lora => {
const name = (lora?.name || '').toLowerCase();
const version = (lora?.version || '').toLowerCase();
return name.includes(query) || version.includes(query);
});

// Sort by most uses
this.filteredLoras = this.filteredLoras.sort((a, b) => b.uses - a.uses);
if (this.loraFavoritesOnly) {
this.filteredLoras = this.filteredLoras.filter(lora => this.isLoraFavorite(lora));
}

// Filter by selected filters
if (this.selectedTags.length > 0) {
this.filteredLoras = this.filteredLoras.filter(lora => lora.tags.some((tag: string) => this.selectedTags.includes(tag)));
}

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

// Function to select a LoRA
Expand All @@ -2556,6 +2665,7 @@ export class OptionsComponent implements OnInit {
lora.strength = 1.0; // Set the default strength to 1.0
this.selectedLoras.push(lora);
this.generationRequest.loras = this.selectedLoras;
this.markLoraLastUsed(lora);

// Add the trigger prompt, if it exists to the prompt input (only if it's not already there)
if (lora.trigger_words) {
Expand Down
Loading