Add AI corrections/refinement to Photo Analysis view
Adds a Corrections box and "Refine with AI" button below the description. User types what the AI got wrong (e.g. "earrings are white gold with diamonds, not silver and zirconium") and clicks Refine — the AI rewrites the title, description and price while keeping all other analysis fields intact. AiAssistantService.RefineWithCorrectionsAsync() sends the current listing text + corrections as a single text prompt and parses the JSON response back into the title/description/price fields. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,54 @@ public class AiAssistantService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Takes an existing AI-generated listing and rewrites title, description and price
|
||||||
|
/// to incorporate user corrections (e.g. "earrings are white gold, not silver").
|
||||||
|
/// Returns the updated fields; other PhotoAnalysisResult fields are unchanged.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(string Title, string Description, decimal Price, string PriceReasoning)>
|
||||||
|
RefineWithCorrectionsAsync(string title, string description, decimal price, string corrections)
|
||||||
|
{
|
||||||
|
var prompt =
|
||||||
|
"You are an expert eBay UK seller. I have a listing with incorrect details that need fixing.\n\n" +
|
||||||
|
$"Current title: {title}\n" +
|
||||||
|
$"Current description:\n{description}\n\n" +
|
||||||
|
$"Current price: £{price:F2}\n\n" +
|
||||||
|
$"CORRECTIONS FROM SELLER: {corrections}\n\n" +
|
||||||
|
"Rewrite the listing incorporating these corrections. " +
|
||||||
|
"Return ONLY valid JSON — no markdown, no explanation:\n" +
|
||||||
|
"{\n" +
|
||||||
|
" \"title\": \"corrected eBay UK title, max 80 chars, keyword-rich\",\n" +
|
||||||
|
" \"description\": \"corrected full plain-text eBay UK description\",\n" +
|
||||||
|
" \"price_suggested\": 0.00,\n" +
|
||||||
|
" \"price_reasoning\": \"one sentence why this price\"\n" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
var json = await CallAsync(prompt);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject obj;
|
||||||
|
try { obj = JObject.Parse(json.Trim()); }
|
||||||
|
catch (JsonReaderException)
|
||||||
|
{
|
||||||
|
var m = System.Text.RegularExpressions.Regex.Match(json, @"```(?:json)?\s*(\{[\s\S]*?\})\s*```");
|
||||||
|
obj = JObject.Parse(m.Success ? m.Groups[1].Value : json.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
obj["title"]?.ToString() ?? title,
|
||||||
|
obj["description"]?.ToString() ?? description,
|
||||||
|
obj["price_suggested"]?.Value<decimal>() ?? price,
|
||||||
|
obj["price_reasoning"]?.ToString() ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Could not parse AI refinement response: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Convenience wrapper — analyses a single photo.</summary>
|
/// <summary>Convenience wrapper — analyses a single photo.</summary>
|
||||||
public Task<PhotoAnalysisResult> AnalyseItemFromPhotoAsync(string imagePath)
|
public Task<PhotoAnalysisResult> AnalyseItemFromPhotoAsync(string imagePath)
|
||||||
=> AnalyseItemFromPhotosAsync(new[] { imagePath });
|
=> AnalyseItemFromPhotosAsync(new[] { imagePath });
|
||||||
|
|||||||
@@ -554,6 +554,47 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Corrections for AI refinement -->
|
||||||
|
<Border BorderThickness="1" CornerRadius="8" Padding="14,10"
|
||||||
|
Margin="0,0,0,10"
|
||||||
|
BorderBrush="{DynamicResource MahApps.Brushes.Gray7}"
|
||||||
|
Background="{DynamicResource MahApps.Brushes.Gray9}">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||||
|
<iconPacks:PackIconMaterial Kind="Pencil" Width="12" Height="12"
|
||||||
|
Margin="0,0,6,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
||||||
|
<TextBlock Text="CORRECTIONS" FontSize="10" FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Accent}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox x:Name="CorrectionsBox"
|
||||||
|
AcceptsReturn="False"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Height="52"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
Style="{DynamicResource MahApps.Styles.TextBox}"
|
||||||
|
FontSize="12"
|
||||||
|
mah:TextBoxHelper.Watermark="e.g. earrings are white gold with diamonds, not silver and zirconium"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
<Button x:Name="RefineBtn"
|
||||||
|
Click="Refine_Click"
|
||||||
|
Style="{DynamicResource MahApps.Styles.Button.Square}"
|
||||||
|
Height="32" Padding="12,0" HorizontalAlignment="Left">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<iconPacks:PackIconMaterial x:Name="RefineIcon" Kind="AutoFix" Width="13" Height="13"
|
||||||
|
Margin="0,0,6,0" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource MahApps.Brushes.Accent}"/>
|
||||||
|
<mah:ProgressRing x:Name="RefineSpinner" Width="13" Height="13"
|
||||||
|
Margin="0,0,6,0" Visibility="Collapsed"/>
|
||||||
|
<TextBlock x:Name="RefineBtnText" Text="Refine with AI"
|
||||||
|
VerticalAlignment="Center" FontSize="12"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Actions + toast overlay -->
|
<!-- Actions + toast overlay -->
|
||||||
<Grid Margin="0,4,0,16" ClipToBounds="False">
|
<Grid Margin="0,4,0,16" ClipToBounds="False">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
|
|||||||
@@ -220,6 +220,63 @@ public partial class PhotoAnalysisView : UserControl
|
|||||||
private void ReAnalyse_Click(object sender, RoutedEventArgs e)
|
private void ReAnalyse_Click(object sender, RoutedEventArgs e)
|
||||||
=> Analyse_Click(sender, e);
|
=> Analyse_Click(sender, e);
|
||||||
|
|
||||||
|
private async void Refine_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_aiService == null || _lastResult == null) return;
|
||||||
|
|
||||||
|
var corrections = CorrectionsBox.Text.Trim();
|
||||||
|
if (string.IsNullOrEmpty(corrections))
|
||||||
|
{
|
||||||
|
CorrectionsBox.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = TitleBox.Text;
|
||||||
|
var description = DescriptionBox.Text;
|
||||||
|
var price = (decimal)(PriceOverride.Value ?? (double)_lastResult.PriceSuggested);
|
||||||
|
|
||||||
|
SetRefining(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (newTitle, newDesc, newPrice, newReasoning) =
|
||||||
|
await _aiService.RefineWithCorrectionsAsync(title, description, price, corrections);
|
||||||
|
|
||||||
|
TitleBox.Text = newTitle;
|
||||||
|
DescriptionBox.Text = newDesc;
|
||||||
|
PriceOverride.Value = (double)newPrice;
|
||||||
|
|
||||||
|
_lastResult.Title = newTitle;
|
||||||
|
_lastResult.Description = newDesc;
|
||||||
|
_lastResult.PriceSuggested = newPrice;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(newReasoning))
|
||||||
|
{
|
||||||
|
PriceReasoningText.Text = newReasoning;
|
||||||
|
PriceReasoningText.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the corrections box now they're applied
|
||||||
|
CorrectionsBox.Text = "";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Refinement failed:\n\n{ex.Message}", "AI Error",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetRefining(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRefining(bool busy)
|
||||||
|
{
|
||||||
|
RefineBtn.IsEnabled = !busy;
|
||||||
|
RefineIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
RefineSpinner.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
RefineBtnText.Text = busy ? "Refining…" : "Refine with AI";
|
||||||
|
}
|
||||||
|
|
||||||
private void ShowResults(PhotoAnalysisResult r)
|
private void ShowResults(PhotoAnalysisResult r)
|
||||||
{
|
{
|
||||||
IdlePanel.Visibility = Visibility.Collapsed;
|
IdlePanel.Visibility = Visibility.Collapsed;
|
||||||
|
|||||||
Reference in New Issue
Block a user