+ }
@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")