Add product image to powder inventory via AI lookup
When AI Lookup fetches a manufacturer product page, it now extracts the og:image (Open Graph) meta tag before stripping HTML tags. The image URL is returned in InventoryAiLookupResult.ImageUrl and automatically shown as a preview on the Create/Edit form alongside the other filled fields. The preview includes a Remove button to clear the image, and the Wrong Match? button clears it along with the other AI-filled fields. On the inventory Details page a product image card is rendered above the Stock & Pricing card whenever ImageUrl is set. The field is nullable so existing records and powders without an image are unaffected. New field: InventoryItem.ImageUrl (nvarchar, nullable). Migration: AddInventoryItemImageUrl. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ public class InventoryItemDto
|
|||||||
public string? ColorFamilies { get; set; }
|
public string? ColorFamilies { get; set; }
|
||||||
public bool RequiresClearCoat { get; set; }
|
public bool RequiresClearCoat { get; set; }
|
||||||
public string? SpecPageUrl { get; set; }
|
public string? SpecPageUrl { get; set; }
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
public decimal QuantityOnHand { get; set; }
|
public decimal QuantityOnHand { get; set; }
|
||||||
public string UnitOfMeasure { get; set; } = "lbs";
|
public string UnitOfMeasure { get; set; } = "lbs";
|
||||||
public decimal ReorderPoint { get; set; }
|
public decimal ReorderPoint { get; set; }
|
||||||
@@ -144,6 +145,10 @@ public class CreateInventoryItemDto
|
|||||||
[Display(Name = "Product URL")]
|
[Display(Name = "Product URL")]
|
||||||
public string? SpecPageUrl { get; set; }
|
public string? SpecPageUrl { get; set; }
|
||||||
|
|
||||||
|
[StringLength(1000, ErrorMessage = "Image URL cannot exceed 1000 characters")]
|
||||||
|
[Display(Name = "Product Image URL")]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
[Range(0, 999999999, ErrorMessage = "Quantity on hand must be 0 or greater")]
|
[Range(0, 999999999, ErrorMessage = "Quantity on hand must be 0 or greater")]
|
||||||
[Display(Name = "Quantity on Hand")]
|
[Display(Name = "Quantity on Hand")]
|
||||||
public decimal QuantityOnHand { get; set; }
|
public decimal QuantityOnHand { get; set; }
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public class InventoryAiLookupResult
|
|||||||
public decimal? UnitCostPerLb { get; set; } // price per lb/unit if found in search results
|
public decimal? UnitCostPerLb { get; set; } // price per lb/unit if found in search results
|
||||||
public string? VendorName { get; set; } // manufacturer/vendor name for dropdown matching
|
public string? VendorName { get; set; } // manufacturer/vendor name for dropdown matching
|
||||||
public string? SpecPageUrl { get; set; } // URL of the product page that was fetched
|
public string? SpecPageUrl { get; set; } // URL of the product page that was fetched
|
||||||
|
public string? ImageUrl { get; set; } // og:image or first product image found on the page
|
||||||
|
|
||||||
public string? Reasoning { get; set; } // brief explanation of what was found
|
public string? Reasoning { get; set; } // brief explanation of what was found
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class InventoryItem : BaseEntity
|
|||||||
public string? ColorFamilies { get; set; } // Comma-separated primary color families e.g. "Green,Blue"
|
public string? ColorFamilies { get; set; } // Comma-separated primary color families e.g. "Green,Blue"
|
||||||
public bool RequiresClearCoat { get; set; } // True if this powder requires a clear coat topcoat
|
public bool RequiresClearCoat { get; set; } // True if this powder requires a clear coat topcoat
|
||||||
public string? SpecPageUrl { get; set; } // Link to manufacturer's product/spec page
|
public string? SpecPageUrl { get; set; } // Link to manufacturer's product/spec page
|
||||||
|
public string? ImageUrl { get; set; } // Product image URL (sourced from og:image on AI lookup)
|
||||||
|
|
||||||
// Sample Panel Tracking (coating category items only)
|
// Sample Panel Tracking (coating category items only)
|
||||||
public bool HasSamplePanel { get; set; } = false;
|
public bool HasSamplePanel { get; set; } = false;
|
||||||
|
|||||||
Generated
+9331
File diff suppressed because it is too large
Load Diff
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddInventoryItemImageUrl : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ImageUrl",
|
||||||
|
table: "InventoryItems",
|
||||||
|
type: "nvarchar(max)",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9171));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9177));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9179));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ImageUrl",
|
||||||
|
table: "InventoryItems");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 0, 14, 747, DateTimeKind.Utc).AddTicks(4877));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 0, 14, 747, DateTimeKind.Utc).AddTicks(4884));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 29, 22, 0, 14, 747, DateTimeKind.Utc).AddTicks(4886));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3181,6 +3181,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Property<bool>("HasSamplePanel")
|
b.Property<bool>("HasSamplePanel")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
b.Property<int?>("InventoryAccountId")
|
b.Property<int?>("InventoryAccountId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
@@ -3732,6 +3735,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Property<int?>("QuoteId")
|
b.Property<int?>("QuoteId")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("QuoteSnapshotUpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
b.Property<decimal>("QuotedPrice")
|
b.Property<decimal>("QuotedPrice")
|
||||||
.HasColumnType("decimal(18,2)");
|
.HasColumnType("decimal(18,2)");
|
||||||
|
|
||||||
@@ -5860,7 +5866,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 28, 16, 40, 22, 359, DateTimeKind.Utc).AddTicks(5055),
|
CreatedAt = new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9171),
|
||||||
Description = "Standard pricing for regular customers",
|
Description = "Standard pricing for regular customers",
|
||||||
DiscountPercent = 0m,
|
DiscountPercent = 0m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -5871,7 +5877,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 28, 16, 40, 22, 359, DateTimeKind.Utc).AddTicks(5063),
|
CreatedAt = new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9177),
|
||||||
Description = "5% discount for preferred customers",
|
Description = "5% discount for preferred customers",
|
||||||
DiscountPercent = 5m,
|
DiscountPercent = 5m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -5882,7 +5888,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 28, 16, 40, 22, 359, DateTimeKind.Utc).AddTicks(5065),
|
CreatedAt = new DateTime(2026, 4, 29, 22, 12, 13, 993, DateTimeKind.Utc).AddTicks(9179),
|
||||||
Description = "10% discount for premium customers",
|
Description = "10% discount for premium customers",
|
||||||
DiscountPercent = 10m,
|
DiscountPercent = 10m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|||||||
@@ -171,14 +171,19 @@ Rules:
|
|||||||
_logger.LogInformation("Using direct manufacturer URL: {Url}", directUrl);
|
_logger.LogInformation("Using direct manufacturer URL: {Url}", directUrl);
|
||||||
|
|
||||||
// Fetch product page
|
// Fetch product page
|
||||||
var pageContent = fetchUrl != null ? await FetchPageTextAsync(fetchUrl) : null;
|
string? pageContent = null;
|
||||||
|
string? pageImageUrl = null;
|
||||||
|
if (fetchUrl != null)
|
||||||
|
{
|
||||||
|
(pageContent, pageImageUrl) = await FetchPageAsync(fetchUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// If direct URL fetch failed, fall back to the search fetch URL
|
// If direct URL fetch failed, fall back to the search fetch URL
|
||||||
if (pageContent == null && directUrl != null && searchFetchUrl != null && searchFetchUrl != directUrl)
|
if (pageContent == null && directUrl != null && searchFetchUrl != null && searchFetchUrl != directUrl)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Direct URL fetch failed; falling back to search URL: {Url}", searchFetchUrl);
|
_logger.LogInformation("Direct URL fetch failed; falling back to search URL: {Url}", searchFetchUrl);
|
||||||
fetchUrl = searchFetchUrl;
|
fetchUrl = searchFetchUrl;
|
||||||
pageContent = await FetchPageTextAsync(searchFetchUrl);
|
(pageContent, pageImageUrl) = await FetchPageAsync(searchFetchUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userPrompt = BuildUserPrompt(manufacturer, colorName, colorCode, partNumber, snippets, fetchUrl, pageContent);
|
var userPrompt = BuildUserPrompt(manufacturer, colorName, colorCode, partNumber, snippets, fetchUrl, pageContent);
|
||||||
@@ -246,6 +251,7 @@ Rules:
|
|||||||
result.UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb");
|
result.UnitCostPerLb = GetDecimal(parsed, "unitCostPerLb");
|
||||||
result.VendorName = GetString(parsed, "vendorName");
|
result.VendorName = GetString(parsed, "vendorName");
|
||||||
result.SpecPageUrl = specPageUrl;
|
result.SpecPageUrl = specPageUrl;
|
||||||
|
result.ImageUrl = pageImageUrl;
|
||||||
result.Reasoning = GetString(parsed, "reasoning");
|
result.Reasoning = GetString(parsed, "reasoning");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -488,7 +494,13 @@ Rules:
|
|||||||
/// A browser-like User-Agent header is sent because some manufacturer sites return 403
|
/// A browser-like User-Agent header is sent because some manufacturer sites return 403
|
||||||
/// or empty responses to bare HttpClient default agents.
|
/// or empty responses to bare HttpClient default agents.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<string?> FetchPageTextAsync(string url)
|
/// <summary>
|
||||||
|
/// Fetches a product page and returns both stripped plain text (for Claude) and the
|
||||||
|
/// best product image URL found on the page. Extracts og:image (Open Graph) first,
|
||||||
|
/// then falls back to twitter:image. The raw HTML is processed before tag-stripping
|
||||||
|
/// so the image URL is captured while it still exists in the markup.
|
||||||
|
/// </summary>
|
||||||
|
private async Task<(string? text, string? imageUrl)> FetchPageAsync(string url)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -499,6 +511,9 @@ Rules:
|
|||||||
|
|
||||||
var html = await client.GetStringAsync(url);
|
var html = await client.GetStringAsync(url);
|
||||||
|
|
||||||
|
// Extract product image from Open Graph / Twitter Card meta tags
|
||||||
|
var imageUrl = ExtractOgImageUrl(html);
|
||||||
|
|
||||||
// Extract structured data (JSON-LD) BEFORE stripping scripts — it contains
|
// Extract structured data (JSON-LD) BEFORE stripping scripts — it contains
|
||||||
// machine-readable price, SKU, and product info that would otherwise be lost.
|
// machine-readable price, SKU, and product info that would otherwise be lost.
|
||||||
var structuredData = ExtractJsonLdData(html);
|
var structuredData = ExtractJsonLdData(html);
|
||||||
@@ -524,17 +539,46 @@ Rules:
|
|||||||
if (!string.IsNullOrWhiteSpace(structuredData))
|
if (!string.IsNullOrWhiteSpace(structuredData))
|
||||||
text = structuredData + "\n" + text;
|
text = structuredData + "\n" + text;
|
||||||
|
|
||||||
_logger.LogInformation("Fetched {Chars} chars from {Url} (structured data: {HasData})",
|
_logger.LogInformation("Fetched {Chars} chars from {Url} (structured data: {HasData}, image: {HasImage})",
|
||||||
text.Length, url, structuredData != null ? "yes" : "no");
|
text.Length, url, structuredData != null ? "yes" : "no", imageUrl != null ? "yes" : "no");
|
||||||
return text;
|
return (text, imageUrl);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to fetch page content from {Url}", url);
|
_logger.LogWarning(ex, "Failed to fetch page content from {Url}", url);
|
||||||
return null;
|
return (null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the best product image URL from raw HTML. Checks og:image first (most
|
||||||
|
/// reliable for e-commerce product pages), then twitter:image as fallback.
|
||||||
|
/// </summary>
|
||||||
|
private static string? ExtractOgImageUrl(string html)
|
||||||
|
{
|
||||||
|
var patterns = new[]
|
||||||
|
{
|
||||||
|
@"<meta[^>]+property=[""']og:image[""'][^>]+content=[""']([^""']+)[""']",
|
||||||
|
@"<meta[^>]+content=[""']([^""']+)[""'][^>]+property=[""']og:image[""']",
|
||||||
|
@"<meta[^>]+name=[""']twitter:image[""'][^>]+content=[""']([^""']+)[""']",
|
||||||
|
@"<meta[^>]+content=[""']([^""']+)[""'][^>]+name=[""']twitter:image[""']",
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var pattern in patterns)
|
||||||
|
{
|
||||||
|
var m = System.Text.RegularExpressions.Regex.Match(
|
||||||
|
html, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
var url = m.Groups[1].Value.Trim();
|
||||||
|
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extracts product name, SKU, and price from JSON-LD structured data blocks.
|
/// Extracts product name, SKU, and price from JSON-LD structured data blocks.
|
||||||
/// Many e-commerce sites (Shopify, WooCommerce, etc.) embed this in the page HTML
|
/// Many e-commerce sites (Shopify, WooCommerce, etc.) embed this in the page HTML
|
||||||
|
|||||||
@@ -123,6 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
|
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<input asp-for="ImageUrl" type="hidden" id="field-imageurl" />
|
||||||
|
<div class="col-12" id="wrap-imagepreview" style="display:@(Model.ImageUrl != null ? "" : "none");">
|
||||||
|
<label class="form-label text-muted small">Product Image (from AI Lookup)</label>
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<img id="field-imagepreview-img" src="@Model.ImageUrl" alt="Product image"
|
||||||
|
style="max-height:120px;max-width:160px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;background:#f8f9fa;padding:4px;" />
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" id="btn-clear-image" title="Remove image">
|
||||||
|
<i class="bi bi-x me-1"></i>Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6" id="wrap-coverage">
|
<div class="col-md-6" id="wrap-coverage">
|
||||||
<div class="d-flex align-items-center gap-1 mb-1">
|
<div class="d-flex align-items-center gap-1 mb-1">
|
||||||
<label asp-for="CoverageSqFtPerLb" class="form-label mb-0">Coverage (@ViewBag.CoverageUnit)</label>
|
<label asp-for="CoverageSqFtPerLb" class="form-label mb-0">Coverage (@ViewBag.CoverageUnit)</label>
|
||||||
|
|||||||
@@ -308,6 +308,16 @@
|
|||||||
|
|
||||||
<!-- Right Column -->
|
<!-- Right Column -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.ImageUrl))
|
||||||
|
{
|
||||||
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
|
<div class="card-body p-3 text-center">
|
||||||
|
<img src="@Model.ImageUrl" alt="@Model.Name"
|
||||||
|
style="max-width:100%;max-height:200px;object-fit:contain;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Stock, Pricing & Status -->
|
<!-- Stock, Pricing & Status -->
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-header bg-white border-0 py-2 d-flex align-items-center gap-2">
|
<div class="card-header bg-white border-0 py-2 d-flex align-items-center gap-2">
|
||||||
|
|||||||
@@ -125,6 +125,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
|
<span asp-validation-for="SpecPageUrl" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<input asp-for="ImageUrl" type="hidden" id="field-imageurl" />
|
||||||
|
<div class="col-12" id="wrap-imagepreview" style="display:@(Model.ImageUrl != null ? "" : "none");">
|
||||||
|
<label class="form-label text-muted small">Product Image (from AI Lookup)</label>
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<img id="field-imagepreview-img" src="@Model.ImageUrl" alt="Product image"
|
||||||
|
style="max-height:120px;max-width:160px;object-fit:contain;border:1px solid #dee2e6;border-radius:6px;background:#f8f9fa;padding:4px;" />
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger" id="btn-clear-image" title="Remove image">
|
||||||
|
<i class="bi bi-x me-1"></i>Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="col-md-6" id="wrap-coverage">
|
<div class="col-md-6" id="wrap-coverage">
|
||||||
<div class="d-flex align-items-center gap-1 mb-1">
|
<div class="d-flex align-items-center gap-1 mb-1">
|
||||||
<label asp-for="CoverageSqFtPerLb" class="form-label mb-0">Coverage (@ViewBag.CoverageUnit)</label>
|
<label asp-for="CoverageSqFtPerLb" class="form-label mb-0">Coverage (@ViewBag.CoverageUnit)</label>
|
||||||
|
|||||||
@@ -153,8 +153,33 @@
|
|||||||
let aiFilledColorFamilies = false;
|
let aiFilledColorFamilies = false;
|
||||||
let aiFilledVendor = false;
|
let aiFilledVendor = false;
|
||||||
let aiFilledClearCoat = false;
|
let aiFilledClearCoat = false;
|
||||||
|
let aiFilledImage = false;
|
||||||
let forceRefill = false; // set true for bad-match retry
|
let forceRefill = false; // set true for bad-match retry
|
||||||
|
|
||||||
|
function setImagePreview(url) {
|
||||||
|
const wrap = document.getElementById('wrap-imagepreview');
|
||||||
|
const img = document.getElementById('field-imagepreview-img');
|
||||||
|
const inp = document.getElementById('field-imageurl');
|
||||||
|
if (!wrap || !img || !inp) return;
|
||||||
|
if (url) {
|
||||||
|
inp.value = url;
|
||||||
|
img.src = url;
|
||||||
|
wrap.style.display = '';
|
||||||
|
} else {
|
||||||
|
inp.value = '';
|
||||||
|
img.src = '';
|
||||||
|
wrap.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearImageBtn = document.getElementById('btn-clear-image');
|
||||||
|
if (clearImageBtn) {
|
||||||
|
clearImageBtn.addEventListener('click', () => {
|
||||||
|
setImagePreview('');
|
||||||
|
aiFilledImage = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function autoComposeName() {
|
function autoComposeName() {
|
||||||
if (!isCoatingCategory(categorySelect?.value)) return;
|
if (!isCoatingCategory(categorySelect?.value)) return;
|
||||||
const color = colorNameEl?.value?.trim() ?? '';
|
const color = colorNameEl?.value?.trim() ?? '';
|
||||||
@@ -257,6 +282,7 @@
|
|||||||
if (cc) cc.checked = false;
|
if (cc) cc.checked = false;
|
||||||
aiFilledClearCoat = false;
|
aiFilledClearCoat = false;
|
||||||
}
|
}
|
||||||
|
if (aiFilledImage) { setImagePreview(''); aiFilledImage = false; }
|
||||||
aiFilledFields = [];
|
aiFilledFields = [];
|
||||||
lastAutoName = '';
|
lastAutoName = '';
|
||||||
forceRefill = false;
|
forceRefill = false;
|
||||||
@@ -421,6 +447,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Product image — show preview if returned and not already set
|
||||||
|
if (data.imageUrl && (forceRefill || !document.getElementById('field-imageurl')?.value?.trim())) {
|
||||||
|
setImagePreview(data.imageUrl);
|
||||||
|
filled.push('Product Image');
|
||||||
|
aiFilledImage = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Build a persistent "needs more info" tip if key identity fields are still unknown
|
// Build a persistent "needs more info" tip if key identity fields are still unknown
|
||||||
const missingHints = [];
|
const missingHints = [];
|
||||||
if (!data.manufacturer && !document.getElementById('field-manufacturer')?.value?.trim())
|
if (!data.manufacturer && !document.getElementById('field-manufacturer')?.value?.trim())
|
||||||
|
|||||||
Reference in New Issue
Block a user