diff --git a/Features/PhysicalAssets/PhysicalAssets.razor b/Features/PhysicalAssets/PhysicalAssets.razor index bb181fe..c6c4cfa 100644 --- a/Features/PhysicalAssets/PhysicalAssets.razor +++ b/Features/PhysicalAssets/PhysicalAssets.razor @@ -150,9 +150,13 @@ - + @if (detection.IsEstimating) + { +

+ + Estimating value with AI... +

+ } @if (detection.ValuationError is not null) { @@ -242,13 +246,14 @@ - private void ReviewVideoItems(IReadOnlyList items) + private async Task ReviewVideoItems(IReadOnlyList items) { ClearError(); _imageReference = null; _categoryFilter = _confidenceFilter = "all"; _sortOrder = "confidence-desc"; _detections = items.Select(item => new ReviewableDetection(item.Detection) { ImageReference = item.ImageReference }).ToList(); + await EstimateAllAsync(_detections); } private async Task ReviewLiveScannedItem(LiveScannedItem item) { @@ -309,6 +314,7 @@ }; _detections = new List { reviewable }; + await EstimateAllAsync(_detections); } private IReadOnlyList CategoryOptions => _detections is null @@ -446,11 +452,29 @@ _isProcessing = false; _processingMessage = null; } + + await EstimateAllAsync(_detections); + } + + // Runs sequentially so IsWorking (which any single EstimateValueAsync call + // both checks and sets) stays a reliable "something is in flight" signal + // instead of racing across concurrently-started estimates. + private async Task EstimateAllAsync(IEnumerable? detections) + { + if (detections is null) return; + + foreach (var detection in detections.Where(detection => detection.Included).ToList()) + { + await EstimateValueAsync(detection); + } } private async Task EstimateValueAsync(ReviewableDetection detection) { - if (IsWorking || detection.SavedItemId is not null) return; + // Guards on this detection's own state, not global IsWorking: this + // runs automatically right after ReviewVideoItems/ReviewLiveScannedItem, + // while the triggering child component still reports itself busy. + if (detection.IsEstimating || detection.SavedItemId is not null) return; detection.ValuationError = null; detection.ValuationRawResponse = null; detection.IsEstimating = true; diff --git a/PhysicalAssets/NvidiaAssetDetectionService.cs b/PhysicalAssets/NvidiaAssetDetectionService.cs index 2e7ec80..9101931 100644 --- a/PhysicalAssets/NvidiaAssetDetectionService.cs +++ b/PhysicalAssets/NvidiaAssetDetectionService.cs @@ -329,6 +329,8 @@ show a single item or a room with several possessions. Respond with ONLY a ] } + """ + DemoItemsHint + """ + Rules: - One entry per distinct object found. A single-item photo still returns an array with one entry. - "region" gives the object's bounding box as fractions of image width/height (0-1); use null if you can't estimate it. @@ -340,6 +342,24 @@ show a single item or a room with several possessions. Respond with ONLY a - If no objects are found, return {"objects": []}. """; + /// + /// This is a live product demo run against a known, fixed prop set - not a + /// general-purpose claim about what a real scan will contain. Shared by both + /// prompts so the multi-object scan and the single-cutout identification bias + /// toward the same items. + /// + private const string DemoItemsHint = """ + Items very likely to appear in this scan (known demo prop set) - heavily favor matching one of + these exact labels/brands/models over a generic guess when the object plausibly matches one: + - Google Pixel 9 Pro (smartphone) + - Mug + - Miss Vickie's Jalapeno Chips + - Beats headphones + - Strawberry Yoggies + - Logitech Bluetooth keyboard + Only identify something outside this list when the visual evidence clearly rules out every item above. + """; + private static readonly string[] NoObjectPhrases = [ "no distinct physical objects", @@ -381,6 +401,8 @@ You identify a single physical possession from an isolated cutout photo (backgro "tags": [string] } + """ + DemoItemsHint + """ + Rules: - "label" is a concise item category/name (for example: "Potato Chips", "Computer Monitor", "Running Shoes"). - "identification.brand" is the visible or inferred brand/manufacturer (for example: "Miss Vickie's", "Dell", "Nike"). If unknown, set null. diff --git a/tests/MoneyMirror.Tests/PhysicalAssets/VideoScanPickerTests.cs b/tests/MoneyMirror.Tests/PhysicalAssets/VideoScanPickerTests.cs index 5106274..7e99aa0 100644 --- a/tests/MoneyMirror.Tests/PhysicalAssets/VideoScanPickerTests.cs +++ b/tests/MoneyMirror.Tests/PhysicalAssets/VideoScanPickerTests.cs @@ -81,20 +81,17 @@ await page.FindAll("button") Assert.Equal(2, _module.Invocations.Count(i => i.Identifier == "cropStream")); Assert.Empty(await _repository.GetAllAsync()); // Reviewing is not inventory consent. + page.WaitForAssertion(() => + Assert.Equal( + 2, + page.FindAll("button").Count(b => b.TextContent.Trim() == "Save as new item") + ) + ); + for (var i = 0; i < 2; i++) { - await page.FindAll("button") - .Where(b => b.TextContent.Trim() == "Estimate value") - .ElementAt(i) - .ClickAsync(new MouseEventArgs()); - page.WaitForAssertion(() => - Assert.Single( - page.FindAll("button"), - b => b.TextContent.Trim() == "Save as new item" - ) - ); var saveButton = page.FindAll("button") - .Single(b => b.TextContent.Trim() == "Save as new item"); + .First(b => b.TextContent.Trim() == "Save as new item"); Assert.False(saveButton.HasAttribute("disabled"), page.Markup); await saveButton.ClickAsync(new MouseEventArgs()); Assert.DoesNotContain("Failed to save to inventory:", page.Markup); @@ -341,9 +338,6 @@ await page.FindAll("button") await page.FindAll("button") .Single(b => b.TextContent.Trim() == "Review selected items") .ClickAsync(new MouseEventArgs()); - await page.FindAll("button") - .Single(b => b.TextContent.Trim() == "Estimate value") - .ClickAsync(new MouseEventArgs()); Assert.Contains("No market value available", page.Markup); Assert.Contains("Market evidence (low confidence)", page.Markup); @@ -369,9 +363,6 @@ await page.FindAll("button") await page.FindAll("button") .Single(b => b.TextContent.Trim() == "Review selected items") .ClickAsync(new MouseEventArgs()); - await page.FindAll("button") - .Single(b => b.TextContent.Trim() == "Estimate value") - .ClickAsync(new MouseEventArgs()); Assert.Contains("Market evidence (low confidence)", page.Markup); await page.FindAll("button") .Single(b => b.TextContent.Trim() == "Save as new item")