DocumentIds { get; set; } = new();
public string Operation { get; set; } // "delete", "activate", "deactivate"
}
-}
\ No newline at end of file
+}
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml
new file mode 100644
index 0000000..edb52a2
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml
@@ -0,0 +1,223 @@
+@{
+ Layout = "_ConfigurePlugin";
+}
+
+@await Component.InvokeAsync("StoreScopeConfiguration")
+
+
+
+
+
+
Image & PDF Text Extraction (OCR)
+
Upload an image or PDF to extract text using OpenAI Vision API.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/VoiceRecorder.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/VoiceRecorder.cshtml
new file mode 100644
index 0000000..643d1bb
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Extras/VoiceRecorder.cshtml
@@ -0,0 +1,176 @@
+@{
+ Layout = "_ConfigurePlugin";
+}
+
+@await Component.InvokeAsync("StoreScopeConfiguration")
+
+
+
+
+
+
Voice Recorder
+
Click the button below to start recording your voice message.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml
index 0083277..4e4ce1b 100644
--- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml
+++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/List.cshtml
@@ -345,7 +345,7 @@
@{
var gridModel = new DataTablesModel
{
- Name = "orders-grid",
+ Name = "orders-grid",
UrlRead = new DataUrl("OrderList", "CustomOrder", null),
SearchButtonId = "search-orders",
Ordering = true,
@@ -400,10 +400,19 @@
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.InnvoiceTechId))
{
Title = "Innvoiceba beküldve",
- Width = "150",
+ Width = "100",
Render = new RenderCustom("renderColumnInnvoiceTechId"),
ClassName = NopColumnClassDefaults.CenterAll
});
+
+ gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsAllOrderItemAvgWeightValid))
+ {
+ Title = "Súlyeltérés",
+ Width = "100",
+ Render = new RenderCustom("renderColumnAverageWeightError"),
+ ClassName = NopColumnClassDefaults.CenterAll
+ });
+
gridModel.ColumnCollection.Add(new ColumnProperty(nameof(OrderModelExtended.IsMeasurable))
{
Title = T($"FruitBank.{nameof(OrderModelExtended.IsMeasurable)}").Text,
@@ -542,7 +551,14 @@
if(data != null) {
return 'Igen';
}
- return 'Nem';
+ return 'Nem';
+ }
+
+ function renderColumnAverageWeightError(data, type, row, meta) {
+ if(data) {
+ return 'OK';
+ }
+ return '!!!';
}
function renderColumnIsMeasurable(data, type, row, meta) {
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml
index a41d112..901e7a7 100644
--- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml
+++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Order/_CustomOrderDetails.Products.cshtml
@@ -159,7 +159,12 @@
Mérés állapota
|
-
+
+ Súlyeltérés
+ |
+
+ Súlyeltérés mértéke
+ |
@*
@T("Admin.Orders.Products.Discount")
| *@
@@ -336,6 +341,24 @@
+
+ @if (!item.AverageWeightIsValid)
+ {
+ !!!
+ }
+ else
+ {
+ OK
+ }
+
+ |
+
+
+
+ Eltérés: @item.AverageWeightDifference (KG), Mért átlag: @item.AverageWeight (KG/rekesz)
+
+
+ |
@*
@if (Model.AllowCustomersToSelectTaxDisplayType)
{
diff --git a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml
index f12452f..c761be2 100644
--- a/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml
+++ b/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/Product/List.cshtml
@@ -285,6 +285,10 @@
{
Title = "Súly(kg)"//T("Admin.Catalog.Products.Fields.NetWeight").Text
},
+ new ColumnProperty(nameof(ProductModelExtended.AverageWeight))
+ {
+ Title = "Átlagsúly (kg)"//T("Admin.Catalog.Products.Fields.AverageWeight").Text
+ },
new ColumnProperty(nameof(ProductModelExtended.Tare))
{
Title = "Tára(kg)"//T("Admin.Catalog.Products.Fields.Tare").Text
diff --git a/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs b/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs
index 0c9d974..a3246cc 100644
--- a/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Components/ProductAttributesViewComponent.cs
@@ -46,6 +46,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Components
model.Tare = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.ProductId, nameof(ITare.Tare));
model.IncomingQuantity = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.ProductId, nameof(IIncomingQuantity.IncomingQuantity));
+ model.AverageWeight = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.ProductId, nameof(IProductDto.AverageWeight));
+ model.AverageWeightTreshold = await _fruitBankAttributeService.GetGenericAttributeValueAsync(model.ProductId, nameof(IProductDto.AverageWeightTreshold));
}
return View("~/Plugins/Misc.FruitBankPlugin/Views/ProductAttributes.cshtml", model);
diff --git a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs
index b637b75..6fcc4ca 100644
--- a/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Domains/EventConsumers/FruitBankEventConsumer.cs
@@ -129,6 +129,20 @@ public class FruitBankEventConsumer :
if (productDto == null || productDto.Tare != tare)
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync(product.Id, nameof(ITare.Tare), tare);
+ //AverageWeight
+ var averageWeight = double.Round(CommonHelper.To(form[nameof(IProductDto.AverageWeight)].ToString()), 1);
+ if (averageWeight < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (averageWeight < 0); productId: {product.Id}; averageWeight: {averageWeight}");
+
+ if (productDto == null || productDto.AverageWeight != averageWeight)
+ await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync(product.Id, nameof(IProductDto.AverageWeight), averageWeight);
+
+ //AverageWeightTreshold
+ var averageWeightTreshold = double.Round(CommonHelper.To(form[nameof(IProductDto.AverageWeightTreshold)].ToString()), 1);
+ if (averageWeightTreshold < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (averageWeightTreshold < 0); productId: {product.Id}; averageWeight: {averageWeightTreshold}");
+
+ if (productDto == null || productDto.AverageWeightTreshold != averageWeightTreshold)
+ await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync(product.Id, nameof(IProductDto.AverageWeightTreshold), averageWeightTreshold);
+
//IncomingQuantity
var incomingQuantity = CommonHelper.To(form[nameof(IIncomingQuantity.IncomingQuantity)].ToString());
if (incomingQuantity < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (incomingQuantity < 0); productId: {product.Id}; incomingQuantity: {incomingQuantity}");
diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
index 2045dd1..3d94a7e 100644
--- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomOrderModelFactory.cs
@@ -182,6 +182,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
orderModelExtended.IsMeasurable = orderDto.IsMeasurable;
orderModelExtended.DateOfReceipt = orderDto.DateOfReceipt;
orderModelExtended.OrderTotal = !orderDto.IsComplete && orderDto.IsMeasurable ? "kalkuláció alatt..." : orderModelExtended.OrderTotal;
+ orderModelExtended.IsAllOrderItemAvgWeightValid = orderDto.IsAllOrderItemAvgWeightValid;
+
//var fullName = $"{orderDto.Customer.FirstName}_{orderDto.Customer.LastName}".Trim();
orderModelExtended.CustomerCompany = $"{orderDto.Customer.Company} {orderDto.Customer.FirstName}_{orderDto.Customer.LastName}";
@@ -226,7 +228,9 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Factories
orderItemModelExtended.ProductStockQuantity = orderItemDto.ProductDto!.StockQuantity;
orderItemModelExtended.ProductIncomingQuantity = orderItemDto.ProductDto.IncomingQuantity;
orderItemModelExtended.ProductAvailableQuantity = orderItemDto.ProductDto.AvailableQuantity;
-
+ orderItemModelExtended.AverageWeight = orderItemDto.AverageWeight;
+ orderItemModelExtended.AverageWeightIsValid = orderItemDto.AverageWeightIsValid;
+ orderItemModelExtended.AverageWeightDifference = orderItemDto.AverageWeightDifference;
orderItemModelExtended.SubTotalInclTax = orderItemDto.IsMeasurable && !orderItemDto.IsAudited ? "kalkuláció alatt..." : orderItemModelExtended.SubTotalInclTax;
diff --git a/Nop.Plugin.Misc.AIPlugin/Factories/CustomProductModelFactory.cs b/Nop.Plugin.Misc.AIPlugin/Factories/CustomProductModelFactory.cs
index 0617f9d..a51148e 100644
--- a/Nop.Plugin.Misc.AIPlugin/Factories/CustomProductModelFactory.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Factories/CustomProductModelFactory.cs
@@ -62,6 +62,8 @@ public class CustomProductModelFactory : MgProductModelFactory();
//services.AddScoped();
services.AddScoped();
+ services.AddScoped();
+
services.AddControllersWithViews(options =>
{
options.Filters.AddService();
diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
index 4b35c8a..7fe2e05 100644
--- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/RouteProvider.cs
@@ -171,6 +171,16 @@ public class RouteProvider : IRouteProvider
name: "Plugin.FruitBank.AppDownload.Download",
pattern: "Admin/AppDownload/Download/{version}/{fileName}",
defaults: new { controller = "AppDownload", action = "Download", area = AreaNames.ADMIN });
+
+ endpointRouteBuilder.MapControllerRoute(
+ name: "Plugin.FruitBank.Admin.FruitBankAudio",
+ pattern: "Admin/VoiceRecorder",
+ defaults: new { controller = "FruitBankAudio", action = "VoiceRecorder", area = AreaNames.ADMIN });
+
+ endpointRouteBuilder.MapControllerRoute(
+ name: "Plugin.FruitBank.Admin.ExtractText",
+ pattern: "Admin/ExtractText",
+ defaults: new { controller = "FileManager", action = "ImageTextExtraction", area = AreaNames.ADMIN });
}
///
diff --git a/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderModelExtended.cs b/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderModelExtended.cs
index fb45d48..a43ba95 100644
--- a/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderModelExtended.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Models/Orders/OrderModelExtended.cs
@@ -14,6 +14,12 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
public int ProductStockQuantity { get; set; }
public int ProductIncomingQuantity { get; set; }
public int ProductAvailableQuantity { get; set; }
+ public double AverageWeight { get; set; }
+ public bool AverageWeightIsValid { get; set; }
+ public double AverageWeightDifference { get; set; }
+
+
+
}
public partial record OrderModelExtended : MgOrderModelExtended, IOrderModelExtended
@@ -28,6 +34,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Orders
public string InnvoiceTechId { get; set; }
+ public bool IsAllOrderItemAvgWeightValid { get; set; }
+
public IList ItemExtendeds { get; set; }
}
}
diff --git a/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs b/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs
index 56726ab..87fd459 100644
--- a/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Models/ProductAttributesModel.cs
@@ -22,5 +22,11 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models
[NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.Tare")]
public double Tare { get; set; }
+
+ [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.AverageWeight")]
+ public double AverageWeight { get; set; }
+
+ [NopResourceDisplayName("Plugins.YourCompany.ProductAttributes.Fields.AverageWeightTreshold")]
+ public double AverageWeightTreshold { get; set; }
}
}
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Models/Products/ProductModelExtended.cs b/Nop.Plugin.Misc.AIPlugin/Models/Products/ProductModelExtended.cs
index 07559a9..30a5f8b 100644
--- a/Nop.Plugin.Misc.AIPlugin/Models/Products/ProductModelExtended.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Models/Products/ProductModelExtended.cs
@@ -13,6 +13,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Models.Products
public int IncomingQuantity { get; set; }
public int AvailableQuantity { get; set; }
+
+ public double AverageWeight { get; set; }
}
}
diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
index c0c9e98..746e23c 100644
--- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
+++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj
@@ -28,13 +28,18 @@
+
-
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -171,6 +176,12 @@
Always
+
+ Always
+
+
+ Always
+
Always
@@ -655,4 +666,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
index 70802ee..c1124da 100644
--- a/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
+++ b/Nop.Plugin.Misc.AIPlugin/Services/OpenAIApiService.cs
@@ -20,6 +20,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
private const string OpenAiEndpoint = "https://api.openai.com/v1/chat/completions";
private const string OpenAiImageEndpoint = "https://api.openai.com/v1/images/generations";
private const string OpenAiFileEndpoint = "https://api.openai.com/v1/files";
+ private const string OpenAiAudioEndpoint = "https://api.openai.com/v1/audio/transcriptions";
private const string BaseUrl = "https://api.openai.com/v1";
private string? _assistantId;
@@ -43,6 +44,79 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
_onError = onErrorCallback;
}
+ #region === AUDIO TRANSCRIPTION ===
+
+ ///
+ /// Transcribe audio file to text using OpenAI Whisper API
+ ///
+ /// The audio file stream
+ /// The original filename (used to determine format)
+ /// Optional language code (e.g., "en", "hu"). If null, auto-detects.
+ /// The transcribed text
+ public async Task TranscribeAudioAsync(Stream audioStream, string fileName, string? language = null)
+ {
+ try
+ {
+ using var form = new MultipartFormDataContent();
+
+ // Add the audio file
+ var audioContent = new StreamContent(audioStream);
+
+ // Determine content type based on file extension
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ audioContent.Headers.ContentType = extension switch
+ {
+ ".mp3" => new MediaTypeHeaderValue("audio/mpeg"),
+ ".mp4" => new MediaTypeHeaderValue("audio/mp4"),
+ ".mpeg" => new MediaTypeHeaderValue("audio/mpeg"),
+ ".mpga" => new MediaTypeHeaderValue("audio/mpeg"),
+ ".m4a" => new MediaTypeHeaderValue("audio/m4a"),
+ ".wav" => new MediaTypeHeaderValue("audio/wav"),
+ ".webm" => new MediaTypeHeaderValue("audio/webm"),
+ _ => new MediaTypeHeaderValue("application/octet-stream")
+ };
+
+ form.Add(audioContent, "file", fileName);
+
+ // Add model
+ form.Add(new StringContent("whisper-1"), "model");
+
+ // Add language if specified
+ if (!string.IsNullOrEmpty(language))
+ {
+ form.Add(new StringContent(language), "language");
+ }
+
+ // Optional: Add response format (json is default, can also be text, srt, verbose_json, or vtt)
+ form.Add(new StringContent("json"), "response_format");
+
+ var response = await _httpClient.PostAsync(OpenAiAudioEndpoint, form);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ Console.WriteLine($"Audio transcription failed: {error}");
+ return null;
+ }
+
+ await using var contentStream = await response.Content.ReadAsStreamAsync();
+ using var json = await JsonDocument.ParseAsync(contentStream);
+
+ var transcription = json.RootElement.GetProperty("text").GetString();
+
+ Console.WriteLine($"Audio transcription successful. Length: {transcription?.Length ?? 0} characters");
+
+ return transcription;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error transcribing audio: {ex}");
+ return null;
+ }
+ }
+
+ #endregion
+
#region === CHAT (TEXT INPUT) ===
public async Task GetSimpleResponseAsync(string systemMessage, string userMessage, string? assistantMessage = null)
@@ -317,24 +391,24 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
Console.WriteLine("Cleaning up all existing vector stores...");
var listRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUrl}/vector_stores");
listRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
-
+
var response = await _httpClient.SendAsync(listRequest);
if (response.IsSuccessStatusCode)
{
using var json = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var vectorStores = json.RootElement.GetProperty("data");
-
+
foreach (var vectorStore in vectorStores.EnumerateArray())
{
var id = vectorStore.GetProperty("id").GetString();
var name = vectorStore.TryGetProperty("name", out var nameElement) && nameElement.ValueKind != JsonValueKind.Null
? nameElement.GetString()
: "Unnamed";
-
+
var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{BaseUrl}/vector_stores/{id}");
deleteRequest.Headers.Add("OpenAI-Beta", "assistants=v2");
await _httpClient.SendAsync(deleteRequest);
-
+
Console.WriteLine($"Deleted vector store: {name} ({id})");
}
Console.WriteLine("Vector store cleanup complete!");
@@ -682,6 +756,99 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Services
}
#endregion
- }
-}
+
+ #region === IMAGE TEXT EXTRACTION ===
+
+ ///
+ /// Extract text from an image using OpenAI Vision API
+ ///
+ /// The image file stream
+ /// The original filename
+ /// Optional custom prompt for text extraction
+ /// Extracted and structured text from the image
+ public async Task ExtractTextFromImageAsync(Stream imageStream, string fileName, string? customPrompt = null)
+ {
+ try
+ {
+ // Read image bytes from stream
+ byte[] imgBytes;
+ using (var memoryStream = new MemoryStream())
+ {
+ await imageStream.CopyToAsync(memoryStream);
+ imgBytes = memoryStream.ToArray();
+ }
+
+ string base64 = Convert.ToBase64String(imgBytes);
+
+ // Determine image format
+ var extension = Path.GetExtension(fileName).ToLowerInvariant();
+ var mimeType = extension switch
+ {
+ ".jpg" or ".jpeg" => "image/jpeg",
+ ".png" => "image/png",
+ ".gif" => "image/gif",
+ ".webp" => "image/webp",
+ _ => "image/jpeg"
+ };
+
+ var prompt = customPrompt ?? "Olvasd ki a szöveget és add vissza szépen strukturálva.";
+
+ var payload = new
+ {
+ model = GetModelName(), // Use the configured model
+ messages = new object[]
+ {
+ new {
+ role = "user",
+ content = new object[]
+ {
+ new { type = "text", text = prompt },
+ new { type = "image_url", image_url = new { url = $"data:{mimeType};base64,{base64}" } }
+ }
+ }
+ }
+ };
+
+ var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ });
+
+ var response = await _httpClient.PostAsync(
+ OpenAiEndpoint,
+ new StringContent(json, Encoding.UTF8, "application/json"));
+
+ if (!response.IsSuccessStatusCode)
+ {
+ var error = await response.Content.ReadAsStringAsync();
+ Console.WriteLine($"Image text extraction failed: {error}");
+ return null;
+ }
+
+ var resultJson = await response.Content.ReadAsStringAsync();
+
+ using var doc = JsonDocument.Parse(resultJson);
+
+ var inputTokens = doc.RootElement.GetProperty("usage").GetProperty("prompt_tokens").GetInt32();
+ var outputTokens = doc.RootElement.GetProperty("usage").GetProperty("completion_tokens").GetInt32();
+ Console.WriteLine($"USAGE STATS - Image OCR Tokens: {inputTokens} + {outputTokens} = {inputTokens + outputTokens}");
+
+ string text = doc.RootElement
+ .GetProperty("choices")[0]
+ .GetProperty("message")
+ .GetProperty("content")
+ .GetString();
+
+ return text;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error extracting text from image: {ex}");
+ return null;
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Nop.Plugin.Misc.AIPlugin/Services/PdfToImageService.cs b/Nop.Plugin.Misc.AIPlugin/Services/PdfToImageService.cs
new file mode 100644
index 0000000..1ffc07f
--- /dev/null
+++ b/Nop.Plugin.Misc.AIPlugin/Services/PdfToImageService.cs
@@ -0,0 +1,96 @@
+using Microsoft.AspNetCore.Hosting;
+using PDFtoImage;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading.Tasks;
+
+public class PdfToImageService
+{
+ private readonly PdfiumBootstrapper _pdfiumBootstrapper;
+
+ public PdfToImageService(IWebHostEnvironment env)
+ {
+ _pdfiumBootstrapper = new PdfiumBootstrapper(env);
+ }
+
+ public async Task> ConvertPdfToJpgAsync(string pdfPath, string outputFolder)
+ {
+ _pdfiumBootstrapper.EnsurePdfiumLoaded();
+
+ var imagePaths = new List();
+
+ try
+ {
+ Directory.CreateDirectory(outputFolder);
+
+ await Task.Run(() =>
+ {
+ byte[] pdfBytes = File.ReadAllBytes(pdfPath);
+
+ var options = new RenderOptions
+ {
+ Dpi = 300,
+ BackgroundColor = SKColors.White
+ };
+
+ var pdfImages = PDFtoImage.Conversion.ToImages(pdfBytes, options: options);
+
+ int pageNumber = 1;
+ foreach (var page in pdfImages)
+ {
+ var outputPath = Path.Combine(outputFolder, $"page_{pageNumber}.jpg");
+
+ using (var fileStream = File.Create(outputPath))
+ {
+ page.Encode(fileStream, SKEncodedImageFormat.Jpeg, 90);
+ }
+
+ imagePaths.Add(outputPath);
+ page.Dispose();
+ pageNumber++;
+ }
+ });
+
+ return imagePaths;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Error converting PDF to images: {ex.Message}", ex);
+ }
+ }
+
+ public class PdfiumBootstrapper
+ {
+ private readonly IWebHostEnvironment _env;
+ private bool _loaded = false;
+
+ public PdfiumBootstrapper(IWebHostEnvironment env)
+ {
+ _env = env;
+ }
+
+ public void EnsurePdfiumLoaded()
+ {
+ if (_loaded)
+ return;
+
+ var pluginPath = Path.Combine(
+ _env.ContentRootPath,
+ "Plugins",
+ "Misc.FruitBankPlugin" // <- change this
+ );
+
+ var archFolder = Environment.Is64BitProcess ? "win-x64" : "win-x86";
+ var dllPath = Path.Combine(pluginPath, "runtimes", archFolder, "native", "pdfium.dll");
+
+ if (!File.Exists(dllPath))
+ throw new FileNotFoundException($"Pdfium.dll not found: {dllPath}");
+
+ NativeLibrary.Load(dllPath);
+ _loaded = true;
+ }
+ }
+}
diff --git a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
index acde179..a167db2 100644
--- a/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
+++ b/Nop.Plugin.Misc.AIPlugin/Views/ProductAttributes.cshtml
@@ -21,7 +21,7 @@
-
+
@@ -46,5 +46,24 @@
+
+
+
\ No newline at end of file
|