Skip to content
Merged
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
34 changes: 29 additions & 5 deletions Features/PhysicalAssets/PhysicalAssets.razor
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,13 @@
</div>
</div>

<button class="btn btn-outline-secondary btn-sm mt-2" @onclick="() => EstimateValueAsync(detection)" disabled="@(IsWorking || detection.SavedItemId is not null)">
@(detection.IsEstimating ? "Estimating value with AI..." : "Estimate value")
</button>
@if (detection.IsEstimating)
{
<p class="text-muted mt-2 mb-0">
<span class="spinner-border spinner-border-sm me-2" aria-hidden="true"></span>
<em>Estimating value with AI...</em>
</p>
}

@if (detection.ValuationError is not null)
{
Expand Down Expand Up @@ -242,13 +246,14 @@



private void ReviewVideoItems(IReadOnlyList<SelectedVideoAsset> items)
private async Task ReviewVideoItems(IReadOnlyList<SelectedVideoAsset> 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)
{
Expand Down Expand Up @@ -309,6 +314,7 @@
};

_detections = new List<ReviewableDetection> { reviewable };
await EstimateAllAsync(_detections);
}

private IReadOnlyList<string> CategoryOptions => _detections is null
Expand Down Expand Up @@ -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<ReviewableDetection>? 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;
Expand Down
22 changes: 22 additions & 0 deletions PhysicalAssets/NvidiaAssetDetectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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": []}.
""";

/// <summary>
/// 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.
/// </summary>
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",
Expand Down Expand Up @@ -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.
Expand Down
25 changes: 8 additions & 17 deletions tests/MoneyMirror.Tests/PhysicalAssets/VideoScanPickerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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")
Expand Down
Loading