Skip to content

Commit 10c0273

Browse files
committed
Fix Extra Networks card duplicating Loras and updated RecommendedModels to use v2 endpoint
1 parent 446be68 commit 10c0273

9 files changed

Lines changed: 318 additions & 213 deletions

File tree

StabilityMatrix.Avalonia/App.axaml.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,16 @@ internal static IServiceCollection ConfigureServices()
660660
new TokenAuthHeaderHandler(serviceProvider.GetRequiredService<LykosAuthTokenProvider>())
661661
);
662662

663+
services
664+
.AddRefitClient<IRecommendedModelsApi>(defaultRefitSettings)
665+
.ConfigureHttpClient(c =>
666+
{
667+
c.BaseAddress = new Uri(LykosAuthApiBaseUrl);
668+
c.Timeout = TimeSpan.FromHours(1);
669+
})
670+
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { AllowAutoRedirect = false })
671+
.AddPolicyHandler(retryPolicy);
672+
663673
services
664674
.AddRefitClient<IPromptGenApi>(defaultRefitSettings)
665675
.ConfigureHttpClient(c =>

StabilityMatrix.Avalonia/DesignData/DesignData.cs

Lines changed: 61 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -459,45 +459,71 @@ public static void Initialize()
459459
public static RecommendedModelsViewModel RecommendedModelsViewModel =>
460460
DialogFactory.Get<RecommendedModelsViewModel>(vm =>
461461
{
462-
vm.Sd15Models = new ObservableCollectionExtended<RecommendedModelItemViewModel>()
463-
{
464-
new()
465-
{
466-
ModelVersion = new CivitModelVersion
462+
// Populate the single RecommendedModels collection for design time
463+
vm.RecommendedModels.AddRange(
464+
[
465+
new RecommendedModelItemViewModel
467466
{
468-
Name = "BB95 Furry Mix",
469-
Description = "A furry mix of BB95",
470-
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
471-
Images =
472-
[
473-
new CivitImage
474-
{
475-
Url =
476-
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
477-
}
478-
],
467+
CivitModel = new CivitModel { Name = "BB95 Furry Mix", Id = 1 }, // Added Id for clarity
468+
ModelVersion = new CivitModelVersion
469+
{
470+
Id = 101, // Added Id for clarity
471+
Name = "v1.0", // Example version name
472+
BaseModel = "SD 1.5", // Example base model
473+
Stats = new CivitModelStats { Rating = 4.5, RatingCount = 124 },
474+
Files = [new CivitFile { Type = CivitFileType.Model }], // Example file
475+
Images =
476+
[
477+
new CivitImage
478+
{
479+
Type = "image", // Ensure type is set
480+
Url =
481+
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
482+
}
483+
],
484+
},
485+
Author = "by bb95"
479486
},
480-
Author = "bb95"
481-
},
482-
new()
483-
{
484-
ModelVersion = new CivitModelVersion
487+
new RecommendedModelItemViewModel
485488
{
486-
Name = "BB95 Furry Mix",
487-
Description = "A furry mix of BB95",
488-
Stats = new CivitModelStats { Rating = 3.5, RatingCount = 24 },
489-
Images =
490-
[
491-
new CivitImage
492-
{
493-
Url =
494-
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/78fd2a0a-42b6-42b0-9815-81cb11bb3d05/00009-2423234823.jpeg"
495-
}
496-
],
489+
CivitModel = new CivitModel { Name = "DreamShaper XL", Id = 2 },
490+
ModelVersion = new CivitModelVersion
491+
{
492+
Id = 201,
493+
Name = "v2.1 Turbo",
494+
BaseModel = "SDXL 1.0",
495+
Stats = new CivitModelStats { Rating = 4.8, RatingCount = 589 },
496+
Files = [new CivitFile { Type = CivitFileType.Model, IsPrimary = true }],
497+
Images =
498+
[
499+
new CivitImage
500+
{
501+
Type = "image",
502+
// Placeholder - replace with an actual relevant image URL if possible
503+
Url =
504+
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0cf3e133-4dde-458b-8a70-7451a3361472/width=450/00016-3919014893.jpeg"
505+
}
506+
],
507+
},
508+
Author = "by Lykon"
497509
},
498-
Author = "bb95"
499-
}
500-
};
510+
new RecommendedModelItemViewModel
511+
{
512+
CivitModel = new CivitModel { Name = "Another Model SD1.5", Id = 3 },
513+
ModelVersion = new CivitModelVersion
514+
{
515+
Id = 301,
516+
Name = "Final",
517+
BaseModel = "SD 1.5",
518+
Stats = new CivitModelStats { Rating = 4.2, RatingCount = 99 },
519+
Files = [new CivitFile { Type = CivitFileType.Model }],
520+
Images = [new CivitImage { Type = "image", Url = Assets.NoImage.ToString() }], // Use placeholder
521+
},
522+
Author = "by Creator3"
523+
}
524+
// Add more items as needed for design-time preview
525+
]
526+
);
501527
});
502528
public static OutputsPageViewModel OutputsPageViewModel
503529
{

StabilityMatrix.Avalonia/ViewModels/Dialogs/RecommendedModelsViewModel.cs

Lines changed: 147 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using StabilityMatrix.Avalonia.Services;
1616
using StabilityMatrix.Avalonia.ViewModels.Base;
1717
using StabilityMatrix.Core.Api;
18+
using StabilityMatrix.Core.Api.LykosAuthApi;
1819
using StabilityMatrix.Core.Attributes;
1920
using StabilityMatrix.Core.Database;
2021
using StabilityMatrix.Core.Extensions;
@@ -30,28 +31,29 @@ namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;
3031
public partial class RecommendedModelsViewModel : ContentDialogViewModelBase
3132
{
3233
private readonly ILogger<RecommendedModelsViewModel> logger;
33-
private readonly ILykosAuthApiV1 lykosApi;
34+
private readonly IRecommendedModelsApi lykosApi;
3435
private readonly ICivitApi civitApi;
3536
private readonly ILiteDbContext liteDbContext;
3637
private readonly ISettingsManager settingsManager;
3738
private readonly INotificationService notificationService;
3839
private readonly ITrackedDownloadService trackedDownloadService;
3940
private readonly IDownloadService downloadService;
4041
private readonly IModelImportService modelImportService;
41-
public SourceCache<RecommendedModelItemViewModel, int> CivitModels { get; } = new(p => p.ModelVersion.Id);
4242

43-
public IObservableCollection<RecommendedModelItemViewModel> Sd15Models { get; set; } =
44-
new ObservableCollectionExtended<RecommendedModelItemViewModel>();
43+
// Single source cache for all models
44+
public SourceCache<RecommendedModelItemViewModel, int> AllRecommendedModelsCache { get; } =
45+
new(p => p.ModelVersion.Id);
4546

46-
public IObservableCollection<RecommendedModelItemViewModel> SdxlModels { get; } =
47+
// Single observable collection bound to the cache
48+
public IObservableCollection<RecommendedModelItemViewModel> RecommendedModels { get; } =
4749
new ObservableCollectionExtended<RecommendedModelItemViewModel>();
4850

4951
[ObservableProperty]
5052
private bool isLoading;
5153

5254
public RecommendedModelsViewModel(
5355
ILogger<RecommendedModelsViewModel> logger,
54-
ILykosAuthApiV1 lykosApi,
56+
IRecommendedModelsApi lykosApi,
5557
ICivitApi civitApi,
5658
ILiteDbContext liteDbContext,
5759
ISettingsManager settingsManager,
@@ -71,20 +73,12 @@ IModelImportService modelImportService
7173
this.downloadService = downloadService;
7274
this.modelImportService = modelImportService;
7375

74-
CivitModels
75-
.Connect()
76-
.DeferUntilLoaded()
77-
.Filter(f => f.ModelVersion.BaseModel == "SD 1.5")
78-
.Bind(Sd15Models)
79-
.ObserveOn(SynchronizationContext.Current)
80-
.Subscribe();
81-
82-
CivitModels
76+
// Bind the single collection to the cache
77+
AllRecommendedModelsCache
8378
.Connect()
8479
.DeferUntilLoaded()
85-
.Filter(f => f.ModelVersion.BaseModel == "SDXL 1.0" || f.ModelVersion.BaseModel == "Pony")
86-
.Bind(SdxlModels)
87-
.ObserveOn(SynchronizationContext.Current)
80+
.Bind(RecommendedModels)
81+
.ObserveOn(SynchronizationContext.Current!) // Use Current! if nullability context allows
8882
.Subscribe();
8983
}
9084

@@ -94,69 +88,168 @@ public override async Task OnLoadedAsync()
9488
return;
9589

9690
IsLoading = true;
91+
AllRecommendedModelsCache.Clear(); // Clear cache before loading
9792

9893
try
9994
{
100-
var recommendedModels = await lykosApi.GetRecommendedModels();
101-
102-
CivitModels.AddOrUpdate(
103-
recommendedModels.Items.Select(
104-
model =>
105-
new RecommendedModelItemViewModel
106-
{
107-
ModelVersion = model.ModelVersions.First(
108-
x =>
109-
!x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase)
110-
&& !x.BaseModel.Contains("Lightning", StringComparison.OrdinalIgnoreCase)
111-
),
112-
Author = $"by {model.Creator?.Username}",
113-
CivitModel = model
114-
}
115-
)
95+
// Call the V2 endpoint
96+
var recommendedModelsResponse = await lykosApi.GetRecommendedModels();
97+
98+
var allModels = recommendedModelsResponse
99+
.RecommendedModelsByCategory.SelectMany(kvp => kvp.Value) // Flatten the dictionary values (lists of models)
100+
.DistinctBy(m => m.Id) // Ensure models appearing in multiple categories are only added once
101+
.Select(model =>
102+
{
103+
// Find the first non-Turbo/Lightning version, or default to the first version if none match
104+
var suitableVersion =
105+
model.ModelVersions?.FirstOrDefault(
106+
x =>
107+
!x.BaseModel.Contains("Turbo", StringComparison.OrdinalIgnoreCase)
108+
&& !x.BaseModel.Contains("Lightning", StringComparison.OrdinalIgnoreCase)
109+
&& x.Files != null
110+
&& x.Files.Any(f => f.Type == CivitFileType.Model) // Ensure there's a model file
111+
)
112+
?? model.ModelVersions?.FirstOrDefault(
113+
x => x.Files != null && x.Files.Any(f => f.Type == CivitFileType.Model)
114+
);
115+
116+
if (suitableVersion == null)
117+
{
118+
logger.LogWarning(
119+
"Model {ModelName} (ID: {ModelId}) has no suitable model version file.",
120+
model.Name,
121+
model.Id
122+
);
123+
return null; // Skip this model if no suitable version found
124+
}
125+
126+
return new RecommendedModelItemViewModel
127+
{
128+
ModelVersion = suitableVersion,
129+
Author = $"by {model.Creator?.Username}",
130+
CivitModel = model
131+
};
132+
})
133+
.Where(vm => vm != null); // Filter out nulls (models skipped due to no suitable version)
134+
135+
AllRecommendedModelsCache.AddOrUpdate(allModels);
136+
}
137+
catch (ApiException apiEx)
138+
{
139+
logger.LogError(
140+
apiEx,
141+
"API Error fetching recommended models V2. Status: {StatusCode}",
142+
apiEx.StatusCode
143+
);
144+
notificationService.Show(
145+
"Failed to get recommended models",
146+
$"Could not reach the server. Please try again later. Error: {apiEx.StatusCode}"
116147
);
148+
OnCloseButtonClick();
117149
}
118150
catch (Exception e)
119151
{
120-
// hide dialog and show error msg
121-
logger.LogError(e, "Failed to get recommended models");
152+
logger.LogError(e, "Failed to get recommended models V2");
122153
notificationService.Show(
123154
"Failed to get recommended models",
124-
"Please try again later or check the Model Browser tab for more models."
155+
"An unexpected error occurred. Please try again later or check the Model Browser tab."
125156
);
126157
OnCloseButtonClick();
127158
}
128-
129-
IsLoading = false;
159+
finally
160+
{
161+
IsLoading = false;
162+
}
130163
}
131164

132165
[RelayCommand]
133166
private async Task DoImport()
134167
{
135-
var selectedModels = SdxlModels.Where(x => x.IsSelected).Concat(Sd15Models.Where(x => x.IsSelected));
168+
var selectedModels = RecommendedModels.Where(x => x.IsSelected).ToList(); // Use the single list
169+
170+
if (!selectedModels.Any())
171+
{
172+
notificationService.Show("No Models Selected", "Please select at least one model to import.");
173+
return;
174+
}
175+
176+
IsLoading = true; // Optionally show loading indicator during import
177+
178+
int successCount = 0;
179+
int failCount = 0;
136180

137181
foreach (var model in selectedModels)
138182
{
139-
// Get latest version file
140-
var modelFile = model.ModelVersion.Files?.FirstOrDefault(
141-
x => x is { Type: CivitFileType.Model, IsPrimary: true }
142-
);
183+
// Get latest version file that is a Model type and marked primary, or fallback to first model file
184+
var modelFile =
185+
model.ModelVersion.Files?.FirstOrDefault(
186+
f => f is { Type: CivitFileType.Model, IsPrimary: true }
187+
) ?? model.ModelVersion.Files?.FirstOrDefault(f => f.Type == CivitFileType.Model);
188+
143189
if (modelFile is null)
144190
{
145-
continue;
191+
logger.LogWarning(
192+
"Skipping import for {ModelName}: No suitable model file found in version {VersionId}.",
193+
model.CivitModel.Name,
194+
model.ModelVersion.Id
195+
);
196+
failCount++;
197+
continue; // Skip if no suitable file
146198
}
147199

148-
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
200+
try
201+
{
202+
var rootModelsDirectory = new DirectoryPath(settingsManager.ModelsDirectory);
203+
var downloadDirectory = rootModelsDirectory.JoinDir(
204+
model.CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
205+
);
206+
207+
await modelImportService.DoImport(
208+
model.CivitModel,
209+
downloadDirectory,
210+
model.ModelVersion,
211+
modelFile
212+
);
213+
successCount++;
214+
model.IsSelected = false; // De-select after successful import start
215+
}
216+
catch (Exception ex)
217+
{
218+
logger.LogError(ex, "Failed to initiate import for model {ModelName}", model.CivitModel.Name);
219+
failCount++;
220+
// Consider notifying the user about the specific failure
221+
notificationService.Show(
222+
"Import Failed",
223+
$"Could not start import for {model.CivitModel.Name}."
224+
);
225+
}
226+
}
149227

150-
var downloadDirectory = rootModelsDirectory.JoinDir(
151-
model.CivitModel.Type.ConvertTo<SharedFolderType>().GetStringValue()
152-
);
228+
IsLoading = false; // Hide loading indicator
153229

154-
await modelImportService.DoImport(
155-
model.CivitModel,
156-
downloadDirectory,
157-
model.ModelVersion,
158-
modelFile
230+
if (failCount == 0 && successCount > 0)
231+
{
232+
notificationService.Show(
233+
"Import Started",
234+
$"{successCount} model(s) added to the download queue."
235+
);
236+
// Optionally close the dialog after successful import initiation
237+
// OnCloseButtonClick();
238+
}
239+
else if (successCount > 0)
240+
{
241+
notificationService.Show(
242+
"Import Partially Started",
243+
$"{successCount} model(s) added to queue. {failCount} failed to start."
244+
);
245+
}
246+
else if (failCount > 0)
247+
{
248+
notificationService.Show(
249+
"Import Failed",
250+
$"Could not start import for {failCount} selected model(s)."
159251
);
160252
}
253+
// else: No models were actually selected or processed, already handled.
161254
}
162255
}

0 commit comments

Comments
 (0)