Merge branch '4.80' of https://git.aycode.com/Adam/Mango.Nop.Plugins into 4.80
This commit is contained in:
commit
91f1d5b9f8
|
|
@ -226,7 +226,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
|||
PaymentStatusIds = paymentStatuses,
|
||||
ShippingStatusIds = shippingStatuses,
|
||||
AvailablePageSizes = "20,50,100,500",
|
||||
|
||||
SortColumnDirection = "desc",
|
||||
});
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Order/List.cshtml", model);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,177 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nop.Core.Domain.Catalog;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
using Nop.Services.Catalog;
|
||||
using Nop.Services.Security;
|
||||
using Nop.Web.Framework;
|
||||
using Nop.Web.Framework.Controllers;
|
||||
using Nop.Web.Framework.Mvc.Filters;
|
||||
using PDFtoImage;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Controllers
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBank.Controllers
|
||||
{
|
||||
public class FileManagerController : Controller
|
||||
[AuthorizeAdmin]
|
||||
[Area(AreaNames.ADMIN)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class FileManagerController : BasePluginController
|
||||
{
|
||||
public IActionResult BindingToFileSystem()
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly OpenAIApiService _aiApiService;
|
||||
private readonly IProductService _productService;
|
||||
private readonly FruitBankDbContext _dbContext;
|
||||
private readonly PdfToImageService _pdfToImageService;
|
||||
|
||||
public FileManagerController(
|
||||
IPermissionService permissionService,
|
||||
OpenAIApiService aiApiService,
|
||||
IProductService productService,
|
||||
FruitBankDbContext fruitBankDbContext,
|
||||
PdfToImageService pdfToImageService)
|
||||
{
|
||||
return View();
|
||||
_permissionService = permissionService;
|
||||
_aiApiService = aiApiService;
|
||||
_productService = productService;
|
||||
_dbContext = fruitBankDbContext;
|
||||
_pdfToImageService = pdfToImageService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display the image text extraction page
|
||||
/// </summary>
|
||||
public async Task<IActionResult> ImageTextExtraction()
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return AccessDeniedView();
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Extras/ImageTextExtraction.cshtml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint to extract text from uploaded image
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ExtractTextFromImage(IFormFile imageFile, string customPrompt = null)
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return Json(new { success = false, message = "Access denied" });
|
||||
|
||||
if (imageFile == null || imageFile.Length == 0)
|
||||
{
|
||||
return Json(new { success = false, message = "No file received" });
|
||||
}
|
||||
|
||||
// Validate file type - now including PDF
|
||||
var extension = Path.GetExtension(imageFile.FileName).ToLowerInvariant();
|
||||
if (extension != ".jpg" && extension != ".jpeg" && extension != ".png" &&
|
||||
extension != ".gif" && extension != ".webp" && extension != ".pdf")
|
||||
{
|
||||
return Json(new { success = false, message = "Invalid file type. Please upload JPG, PNG, GIF, WebP, or PDF." });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Define the uploads folder
|
||||
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "ocr");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
}
|
||||
|
||||
string processedFilePath;
|
||||
string processedFileName;
|
||||
|
||||
// Handle PDF conversion
|
||||
if (extension == ".pdf")
|
||||
{
|
||||
// Save the PDF temporarily
|
||||
var tempPdfFileName = $"temp_pdf_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
|
||||
var tempPdfPath = Path.Combine(uploadsFolder, tempPdfFileName);
|
||||
|
||||
using (var stream = new FileStream(tempPdfPath, FileMode.Create))
|
||||
{
|
||||
await imageFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// Convert PDF to JPG using our service
|
||||
var convertedImages = await _pdfToImageService.ConvertPdfToJpgAsync(tempPdfPath, uploadsFolder);
|
||||
|
||||
if (convertedImages == null || convertedImages.Count == 0)
|
||||
{
|
||||
// Clean up temp PDF
|
||||
if (System.IO.File.Exists(tempPdfPath))
|
||||
System.IO.File.Delete(tempPdfPath);
|
||||
|
||||
return Json(new { success = false, message = "Failed to convert PDF or PDF is empty" });
|
||||
}
|
||||
|
||||
// Use the first page
|
||||
processedFilePath = convertedImages[0];
|
||||
processedFileName = Path.GetFileName(processedFilePath);
|
||||
|
||||
// Clean up temp PDF
|
||||
if (System.IO.File.Exists(tempPdfPath))
|
||||
System.IO.File.Delete(tempPdfPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle regular image files
|
||||
processedFileName = $"ocr_image_{DateTime.Now:yyyyMMdd_HHmmss}{extension}";
|
||||
processedFilePath = Path.Combine(uploadsFolder, processedFileName);
|
||||
|
||||
using (var stream = new FileStream(processedFilePath, FileMode.Create))
|
||||
{
|
||||
await imageFile.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract text from the processed image using OpenAI Vision API
|
||||
string extractedText;
|
||||
using (var imageStream = new FileStream(processedFilePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
extractedText = await _aiApiService.ExtractTextFromImageAsync(
|
||||
imageStream,
|
||||
processedFileName,
|
||||
customPrompt
|
||||
);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(extractedText))
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
success = false,
|
||||
message = "Failed to extract text. The API may have returned an empty response."
|
||||
});
|
||||
}
|
||||
|
||||
return Json(new
|
||||
{
|
||||
success = true,
|
||||
message = extension == ".pdf"
|
||||
? "PDF converted and text extracted successfully"
|
||||
: "Text extracted successfully",
|
||||
extractedText = extractedText,
|
||||
fileName = processedFileName,
|
||||
filePath = processedFilePath,
|
||||
fileSize = imageFile.Length,
|
||||
wasConverted = extension == ".pdf"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"Error in ExtractTextFromImage: {ex}");
|
||||
return Json(new
|
||||
{
|
||||
success = false,
|
||||
message = $"Error processing file: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,160 @@
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Domains.DataLayer;
|
||||
using Nop.Plugin.Misc.FruitBankPlugin.Services;
|
||||
using Nop.Services.Security;
|
||||
using Nop.Web.Framework;
|
||||
using Nop.Web.Framework.Controllers;
|
||||
using Nop.Web.Framework.Mvc.Filters;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Nop.Plugin.Misc.FruitBank.Controllers
|
||||
{
|
||||
[AuthorizeAdmin]
|
||||
[Area(AreaNames.ADMIN)]
|
||||
[AutoValidateAntiforgeryToken]
|
||||
public class FruitBankAudioController : BasePluginController
|
||||
{
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly OpenAIApiService _aiApiService;
|
||||
private readonly FruitBankDbContext _dbContext;
|
||||
|
||||
public FruitBankAudioController(
|
||||
IPermissionService permissionService,
|
||||
OpenAIApiService aiApiService,
|
||||
FruitBankDbContext fruitBankDbContext)
|
||||
{
|
||||
_permissionService = permissionService;
|
||||
_aiApiService = aiApiService;
|
||||
_dbContext = fruitBankDbContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display the voice recorder page
|
||||
/// </summary>
|
||||
public async Task<IActionResult> VoiceRecorder()
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return AccessDeniedView();
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Areas/Admin/Views/Extras/VoiceRecorder.cshtml");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint to receive voice recording
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ReceiveVoiceRecording(IFormFile audioFile)
|
||||
{
|
||||
if (!await _permissionService.AuthorizeAsync(StandardPermission.Security.ACCESS_ADMIN_PANEL))
|
||||
return Json(new { success = false, message = "Access denied" });
|
||||
|
||||
if (audioFile == null || audioFile.Length == 0)
|
||||
{
|
||||
return Json(new { success = false, message = "No audio file received" });
|
||||
}
|
||||
|
||||
string productDataSummary = string.Empty;
|
||||
string orderDataSummary = string.Empty;
|
||||
|
||||
var products = await _dbContext.Products.GetAll().ToListAsync();
|
||||
foreach (var product in products)
|
||||
{
|
||||
//let's prepare basic product and stock information for AI analysis
|
||||
if (product == null) continue;
|
||||
|
||||
var productName = product.Name;
|
||||
var stockQuantity = product.StockQuantity;
|
||||
|
||||
productDataSummary += $"Product: {productName}, Stock Quantity: {stockQuantity}\n";
|
||||
|
||||
}
|
||||
|
||||
var orders = await _dbContext.OrderDtos.GetAll(true).ToListAsync();
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
//let's prepare basic order information for AI analysis
|
||||
if (order == null) continue;
|
||||
var orderId = order.Id;
|
||||
var customerName = order.Customer.Company;
|
||||
var totalAmount = order.OrderTotal;
|
||||
var dateofReceipt = order.DateOfReceipt;
|
||||
var isMeasurable = order.IsMeasurable;
|
||||
var itemsInOrder = order.OrderItemDtos.Count;
|
||||
orderDataSummary += $"Order ID: {orderId}, Customer: {customerName}, Total Amount: {totalAmount}, Date of Receipt: {dateofReceipt}, Is Measurable: {isMeasurable}, Items in Order: {itemsInOrder}\n";
|
||||
}
|
||||
|
||||
|
||||
Console.WriteLine("Product Data Summary: " + productDataSummary);
|
||||
|
||||
try
|
||||
{
|
||||
// Generate a unique filename
|
||||
var fileName = $"voice_recording_{DateTime.Now:yyyyMMdd_HHmmss}.webm";
|
||||
|
||||
// Define the path where you want to save the file
|
||||
var uploadsFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "voice");
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
if (!Directory.Exists(uploadsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(uploadsFolder);
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(uploadsFolder, fileName);
|
||||
|
||||
// Save the file locally
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await audioFile.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// Transcribe the audio using OpenAI Whisper API
|
||||
string transcribedText;
|
||||
using (var audioStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
transcribedText = await _aiApiService.TranscribeAudioAsync(audioStream, fileName, "en"); // or "hu" for Hungarian
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(transcribedText))
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
success = false,
|
||||
message = "Failed to transcribe audio"
|
||||
});
|
||||
}
|
||||
|
||||
string SystemMessage = "You are an assistant that helps with analyzing data for Fruitbank, a fruit trading company based in Hungary. " +
|
||||
"Provide insights and suggestions based on the provided data." +
|
||||
$"Products information: {productDataSummary}" +
|
||||
$"Orders information: {orderDataSummary}";
|
||||
|
||||
|
||||
var AIResponse = await _aiApiService.GetSimpleResponseAsync(SystemMessage, $"Transcribed Text: {transcribedText}");
|
||||
|
||||
|
||||
|
||||
return Json(new
|
||||
{
|
||||
success = true,
|
||||
message = $"Audio transcribed successfully",
|
||||
transcription = AIResponse,
|
||||
filePath = filePath,
|
||||
fileSize = audioFile.Length
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
success = false,
|
||||
message = $"Error processing audio: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
|
|||
public record EditShippingModel : BaseNopModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
|
||||
|
||||
public string LicencePlate { get; set; }
|
||||
|
||||
|
|
@ -61,4 +61,4 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Areas.Admin.Models
|
|||
public List<int> DocumentIds { get; set; } = new();
|
||||
public string Operation { get; set; } // "delete", "activate", "deactivate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,223 @@
|
|||
@{
|
||||
Layout = "_ConfigurePlugin";
|
||||
}
|
||||
|
||||
@await Component.InvokeAsync("StoreScopeConfiguration")
|
||||
|
||||
<form asp-action="ExtractTextFromImage" asp-controller="FileManager" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
</form>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-body">
|
||||
<h4>Image & PDF Text Extraction (OCR)</h4>
|
||||
<p>Upload an image or PDF to extract text using OpenAI Vision API.</p>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<div class="custom-file">
|
||||
<input type="file" class="custom-file-input" id="imageFile" accept="image/*,.pdf,application/pdf">
|
||||
<label class="custom-file-label" for="imageFile">Choose image or PDF file...</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">Supported formats: JPG, PNG, GIF, WebP, PDF</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<label for="customPrompt">Custom Prompt (Optional)</label>
|
||||
<input type="text" class="form-control" id="customPrompt" placeholder="Leave empty for default: 'Olvasd ki a szöveget és add vissza szépen strukturálva.'">
|
||||
<small class="form-text text-muted">Customize how the AI should extract and format the text</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<button type="button" id="uploadButton" class="btn btn-primary" disabled>
|
||||
<i class="fas fa-upload"></i> Extract Text
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<div id="responseMessage" class="alert" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" id="filePreviewSection" style="display:none;">
|
||||
<div class="col-md-9">
|
||||
<h5>File Preview:</h5>
|
||||
<div id="imagePreviewContainer" style="display:none;">
|
||||
<img id="imagePreview" src="" alt="Preview" style="max-width: 100%; max-height: 400px; border: 1px solid #ddd; padding: 5px;">
|
||||
</div>
|
||||
<div id="pdfPreviewContainer" style="display:none;">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-file-pdf"></i> PDF file selected. The first page will be converted to image for text extraction.
|
||||
</div>
|
||||
<embed id="pdfPreview" type="application/pdf" style="width: 100%; height: 500px; border: 1px solid #ddd;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" id="extractedTextSection" style="display:none;">
|
||||
<div class="col-md-9">
|
||||
<h5>Extracted Text:</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<pre id="extractedText" style="white-space: pre-wrap; word-wrap: break-word; max-height: 500px; overflow-y: auto;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="copyButton" class="btn btn-secondary mt-2">
|
||||
<i class="fas fa-copy"></i> Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const imageFileInput = document.getElementById('imageFile');
|
||||
const customPromptInput = document.getElementById('customPrompt');
|
||||
const uploadButton = document.getElementById('uploadButton');
|
||||
const responseMessage = document.getElementById('responseMessage');
|
||||
const filePreviewSection = document.getElementById('filePreviewSection');
|
||||
const imagePreviewContainer = document.getElementById('imagePreviewContainer');
|
||||
const pdfPreviewContainer = document.getElementById('pdfPreviewContainer');
|
||||
const imagePreview = document.getElementById('imagePreview');
|
||||
const pdfPreview = document.getElementById('pdfPreview');
|
||||
const extractedTextSection = document.getElementById('extractedTextSection');
|
||||
const extractedText = document.getElementById('extractedText');
|
||||
const copyButton = document.getElementById('copyButton');
|
||||
const fileLabel = document.querySelector('.custom-file-label');
|
||||
|
||||
let selectedFile = null;
|
||||
|
||||
// Update file label and enable upload button when file is selected
|
||||
imageFileInput.addEventListener('change', (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
selectedFile = file;
|
||||
fileLabel.textContent = file.name;
|
||||
uploadButton.disabled = false;
|
||||
|
||||
// Check file type and show appropriate preview
|
||||
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
|
||||
|
||||
if (isPdf) {
|
||||
// Show PDF preview
|
||||
imagePreviewContainer.style.display = 'none';
|
||||
pdfPreviewContainer.style.display = 'block';
|
||||
|
||||
const fileUrl = URL.createObjectURL(file);
|
||||
pdfPreview.src = fileUrl;
|
||||
|
||||
filePreviewSection.style.display = 'block';
|
||||
} else {
|
||||
// Show image preview
|
||||
pdfPreviewContainer.style.display = 'none';
|
||||
imagePreviewContainer.style.display = 'block';
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
imagePreview.src = e.target.result;
|
||||
filePreviewSection.style.display = 'block';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
} else {
|
||||
selectedFile = null;
|
||||
fileLabel.textContent = 'Choose image or PDF file...';
|
||||
uploadButton.disabled = true;
|
||||
filePreviewSection.style.display = 'none';
|
||||
}
|
||||
extractedTextSection.style.display = 'none';
|
||||
});
|
||||
|
||||
uploadButton.addEventListener('click', async () => {
|
||||
if (!selectedFile) {
|
||||
showMessage('Please select a file first!', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('imageFile', selectedFile);
|
||||
|
||||
const customPrompt = customPromptInput.value.trim();
|
||||
if (customPrompt) {
|
||||
formData.append('customPrompt', customPrompt);
|
||||
}
|
||||
|
||||
try {
|
||||
uploadButton.disabled = true;
|
||||
const isPdf = selectedFile.type === 'application/pdf' || selectedFile.name.toLowerCase().endsWith('.pdf');
|
||||
uploadButton.innerHTML = isPdf
|
||||
? '<i class="fas fa-spinner fa-spin"></i> Converting PDF & Extracting...'
|
||||
: '<i class="fas fa-spinner fa-spin"></i> Extracting...';
|
||||
|
||||
// Get the antiforgery token
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||
|
||||
const response = await fetch('@Url.Action("ExtractTextFromImage", "FileManager")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'RequestVerificationToken': token
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
const message = result.wasConverted
|
||||
? 'PDF converted and text extracted successfully!'
|
||||
: 'Text extracted successfully!';
|
||||
showMessage(message, 'success');
|
||||
|
||||
// Display extracted text
|
||||
if (result.extractedText) {
|
||||
extractedText.textContent = result.extractedText;
|
||||
extractedTextSection.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
showMessage('Error: ' + (result.message || 'Failed to extract text'), 'danger');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error extracting text:', error);
|
||||
showMessage('Error: Failed to communicate with server', 'danger');
|
||||
} finally {
|
||||
uploadButton.disabled = false;
|
||||
uploadButton.innerHTML = '<i class="fas fa-upload"></i> Extract Text';
|
||||
}
|
||||
});
|
||||
|
||||
copyButton.addEventListener('click', () => {
|
||||
const textToCopy = extractedText.textContent;
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
const originalText = copyButton.innerHTML;
|
||||
copyButton.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||||
copyButton.classList.remove('btn-secondary');
|
||||
copyButton.classList.add('btn-success');
|
||||
|
||||
setTimeout(() => {
|
||||
copyButton.innerHTML = originalText;
|
||||
copyButton.classList.remove('btn-success');
|
||||
copyButton.classList.add('btn-secondary');
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text:', err);
|
||||
showMessage('Failed to copy to clipboard', 'warning');
|
||||
});
|
||||
});
|
||||
|
||||
function showMessage(message, type) {
|
||||
responseMessage.textContent = message;
|
||||
responseMessage.className = 'alert alert-' + type;
|
||||
responseMessage.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
responseMessage.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
@{
|
||||
Layout = "_ConfigurePlugin";
|
||||
}
|
||||
|
||||
@await Component.InvokeAsync("StoreScopeConfiguration")
|
||||
|
||||
<form asp-action="ReceiveVoiceRecording" asp-controller="FruitBankAdmin" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
</form>
|
||||
|
||||
<div class="card card-default">
|
||||
<div class="card-body">
|
||||
<h4>Voice Recorder</h4>
|
||||
<p>Click the button below to start recording your voice message.</p>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<button type="button" id="recordButton" class="btn btn-primary">
|
||||
<i class="fas fa-microphone"></i> Start Recording
|
||||
</button>
|
||||
<button type="button" id="stopButton" class="btn btn-danger" style="display:none;">
|
||||
<i class="fas fa-stop"></i> Stop Recording
|
||||
</button>
|
||||
<span id="recordingStatus" style="margin-left: 15px; font-weight: bold;"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" id="audioPlaybackSection" style="display:none;">
|
||||
<div class="col-md-9">
|
||||
<audio id="audioPlayback" controls style="width: 100%;"></audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" id="sendSection" style="display:none;">
|
||||
<div class="col-md-9">
|
||||
<button type="button" id="sendButton" class="btn btn-success">
|
||||
<i class="fas fa-paper-plane"></i> Send to API
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-md-9">
|
||||
<div id="responseMessage" class="alert" style="display:none;"></div>
|
||||
<div id="transcriptionResult" style="display:none; margin-top: 15px;">
|
||||
<h5>Transcription:</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p id="transcriptionText" style="white-space: pre-wrap;"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let mediaRecorder;
|
||||
let audioChunks = [];
|
||||
let audioBlob;
|
||||
|
||||
const recordButton = document.getElementById('recordButton');
|
||||
const stopButton = document.getElementById('stopButton');
|
||||
const sendButton = document.getElementById('sendButton');
|
||||
const recordingStatus = document.getElementById('recordingStatus');
|
||||
const audioPlayback = document.getElementById('audioPlayback');
|
||||
const audioPlaybackSection = document.getElementById('audioPlaybackSection');
|
||||
const sendSection = document.getElementById('sendSection');
|
||||
const responseMessage = document.getElementById('responseMessage');
|
||||
const transcriptionResult = document.getElementById('transcriptionResult');
|
||||
const transcriptionText = document.getElementById('transcriptionText');
|
||||
|
||||
recordButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
audioChunks = [];
|
||||
|
||||
mediaRecorder.ondataavailable = (event) => {
|
||||
audioChunks.push(event.data);
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = () => {
|
||||
audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
audioPlayback.src = audioUrl;
|
||||
audioPlaybackSection.style.display = 'block';
|
||||
sendSection.style.display = 'block';
|
||||
|
||||
// Stop all tracks to release the microphone
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
recordButton.style.display = 'none';
|
||||
stopButton.style.display = 'inline-block';
|
||||
recordingStatus.textContent = 'Recording...';
|
||||
recordingStatus.style.color = 'red';
|
||||
audioPlaybackSection.style.display = 'none';
|
||||
sendSection.style.display = 'none';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error accessing microphone:', error);
|
||||
showMessage('Error: Could not access microphone. Please check permissions.', 'danger');
|
||||
}
|
||||
});
|
||||
|
||||
stopButton.addEventListener('click', () => {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
recordButton.style.display = 'inline-block';
|
||||
stopButton.style.display = 'none';
|
||||
recordingStatus.textContent = 'Recording stopped';
|
||||
recordingStatus.style.color = 'green';
|
||||
}
|
||||
});
|
||||
|
||||
sendButton.addEventListener('click', async () => {
|
||||
if (!audioBlob) {
|
||||
showMessage('No audio recorded yet!', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('audioFile', audioBlob, 'recording.webm');
|
||||
|
||||
try {
|
||||
sendButton.disabled = true;
|
||||
sendButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
||||
|
||||
// Get the antiforgery token
|
||||
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
|
||||
|
||||
const response = await fetch('@Url.Action("ReceiveVoiceRecording", "FruitBankAudio")', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'RequestVerificationToken': token
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showMessage('Audio transcribed successfully!', 'success');
|
||||
|
||||
// Display transcription
|
||||
if (result.transcription) {
|
||||
transcriptionText.textContent = result.transcription;
|
||||
transcriptionResult.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
showMessage('Error: ' + (result.message || 'Failed to transcribe audio'), 'danger');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error sending audio:', error);
|
||||
showMessage('Error: Failed to send audio to server', 'danger');
|
||||
} finally {
|
||||
sendButton.disabled = false;
|
||||
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i> Send to API';
|
||||
}
|
||||
});
|
||||
|
||||
function showMessage(message, type) {
|
||||
responseMessage.textContent = message;
|
||||
responseMessage.className = 'alert alert-' + type;
|
||||
responseMessage.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
responseMessage.style.display = 'none';
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -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 '<span class="badge badge-success" disabled>Igen</span>';
|
||||
}
|
||||
return '<span class="badge badge-warning" disabled>Nem</span>';
|
||||
return '<span class="badge badge-danger" disabled>Nem</span>';
|
||||
}
|
||||
|
||||
function renderColumnAverageWeightError(data, type, row, meta) {
|
||||
if(data) {
|
||||
return '<span class="badge badge-success" disabled>OK</span>';
|
||||
}
|
||||
return '<span class="badge badge-danger" disabled>!!!</span>';
|
||||
}
|
||||
|
||||
function renderColumnIsMeasurable(data, type, row, meta) {
|
||||
|
|
|
|||
|
|
@ -159,7 +159,12 @@
|
|||
<th>
|
||||
Mérés állapota
|
||||
</th>
|
||||
|
||||
<th>
|
||||
Súlyeltérés
|
||||
</th>
|
||||
<th>
|
||||
Súlyeltérés mértéke
|
||||
</th>
|
||||
@* <th>
|
||||
@T("Admin.Orders.Products.Discount")
|
||||
</th> *@
|
||||
|
|
@ -336,6 +341,24 @@
|
|||
</div>
|
||||
</td>
|
||||
|
||||
<td style="width: 80px;" class="text-center">
|
||||
@if (!item.AverageWeightIsValid)
|
||||
{
|
||||
<span class="badge badge-danger" disabled>!!!</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge badge-success" disabled>OK</span>
|
||||
}
|
||||
<input type="hidden" name="pvAverageWeightIsValid@(item.Id)" id="pvAverageWeightIsValid@(item.Id)" value="@(item.AverageWeightIsValid.ToString())" disabled />
|
||||
</td>
|
||||
<td style="width: 80px;" class="text-center">
|
||||
|
||||
|
||||
<span class="">Eltérés: @item.AverageWeightDifference (KG), Mért átlag: @item.AverageWeight (KG/rekesz)</span>
|
||||
|
||||
<input type="hidden" name="pvAverageWeightDifference@(item.Id)" id="pvAverageWeightDifference@(item.Id)" value="@(item.AverageWeightDifference.ToString())" disabled />
|
||||
</td>
|
||||
@* <td style="width: 15%;" class="text-center">
|
||||
@if (Model.AllowCustomersToSelectTaxDisplayType)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ namespace Nop.Plugin.Misc.FruitBankPlugin.Components
|
|||
|
||||
model.Tare = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(ITare.Tare));
|
||||
model.IncomingQuantity = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, int>(model.ProductId, nameof(IIncomingQuantity.IncomingQuantity));
|
||||
model.AverageWeight = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(IProductDto.AverageWeight));
|
||||
model.AverageWeightTreshold = await _fruitBankAttributeService.GetGenericAttributeValueAsync<Product, double>(model.ProductId, nameof(IProductDto.AverageWeightTreshold));
|
||||
}
|
||||
|
||||
return View("~/Plugins/Misc.FruitBankPlugin/Views/ProductAttributes.cshtml", model);
|
||||
|
|
|
|||
|
|
@ -129,6 +129,20 @@ public class FruitBankEventConsumer :
|
|||
if (productDto == null || productDto.Tare != tare)
|
||||
await _fruitBankAttributeService.InsertOrUpdateGenericAttributeAsync<Product, double>(product.Id, nameof(ITare.Tare), tare);
|
||||
|
||||
//AverageWeight
|
||||
var averageWeight = double.Round(CommonHelper.To<double>(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, double>(product.Id, nameof(IProductDto.AverageWeight), averageWeight);
|
||||
|
||||
//AverageWeightTreshold
|
||||
var averageWeightTreshold = double.Round(CommonHelper.To<double>(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, double>(product.Id, nameof(IProductDto.AverageWeightTreshold), averageWeightTreshold);
|
||||
|
||||
//IncomingQuantity
|
||||
var incomingQuantity = CommonHelper.To<int>(form[nameof(IIncomingQuantity.IncomingQuantity)].ToString());
|
||||
if (incomingQuantity < 0) throw new Exception($"FruitBankEventConsumer->SaveProductCustomAttributesAsync(); (incomingQuantity < 0); productId: {product.Id}; incomingQuantity: {incomingQuantity}");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ public class CustomProductModelFactory : MgProductModelFactory<ProductListModelE
|
|||
productModelExtended.AvailableQuantity = productDto.AvailableQuantity;
|
||||
|
||||
productModelExtended.StockQuantityStr = productModelExtended.StockQuantity.ToString();
|
||||
|
||||
productModelExtended.AverageWeight = productDto.AverageWeight;
|
||||
});
|
||||
|
||||
return productListModelExtended;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ public class PluginNopStartup : INopStartup
|
|||
services.AddScoped<OpenAIApiService>();
|
||||
//services.AddScoped<IAIAPIService, OpenAIApiService>();
|
||||
services.AddScoped<AICalculationService>();
|
||||
services.AddScoped<PdfToImageService>();
|
||||
|
||||
services.AddControllersWithViews(options =>
|
||||
{
|
||||
options.Filters.AddService<PendingMeasurementCheckoutFilter>();
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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<OrderItemModelExtended> ItemExtendeds { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,13 +28,18 @@
|
|||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />
|
||||
|
||||
<!--<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />-->
|
||||
<PackageReference Include="PdfPig" Version="0.1.11" />
|
||||
<PackageReference Include="PdfPig.Rendering.Skia" Version="0.1.11.5" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="System.Memory.Data" Version="9.0.10" />
|
||||
<PackageReference Include="Tesseract" Version="5.2.0" />
|
||||
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
||||
<PackageReference Include="TesseractOCR" Version="5.5.1" />
|
||||
<PackageReference Include="PDFtoImage" Version="4.0.2" />
|
||||
<PackageReference Include="PDFtoImage" Version="4.0.2">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
@ -171,6 +176,12 @@
|
|||
<None Update="Areas\Admin\Views\AppDownload\Index.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Areas\Admin\Views\Extras\ImageTextExtraction.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Areas\Admin\Views\Extras\VoiceRecorder.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Areas\Admin\Views\Order\FileUploadGridComponent.cshtml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
|
@ -655,4 +666,10 @@
|
|||
<Target Name="NopTarget" AfterTargets="Build">
|
||||
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
||||
</Target>
|
||||
<Target Name="CopyPdfiumDll" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<PdfiumFiles Include="$(NuGetPackageRoot)pdfium.binaries\**\pdfium.dll" />
|
||||
</ItemGroup>
|
||||
<Copy SourceFiles="@(PdfiumFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -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 ===
|
||||
|
||||
/// <summary>
|
||||
/// Transcribe audio file to text using OpenAI Whisper API
|
||||
/// </summary>
|
||||
/// <param name="audioStream">The audio file stream</param>
|
||||
/// <param name="fileName">The original filename (used to determine format)</param>
|
||||
/// <param name="language">Optional language code (e.g., "en", "hu"). If null, auto-detects.</param>
|
||||
/// <returns>The transcribed text</returns>
|
||||
public async Task<string?> 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<string?> 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 ===
|
||||
|
||||
/// <summary>
|
||||
/// Extract text from an image using OpenAI Vision API
|
||||
/// </summary>
|
||||
/// <param name="imageStream">The image file stream</param>
|
||||
/// <param name="fileName">The original filename</param>
|
||||
/// <param name="customPrompt">Optional custom prompt for text extraction</param>
|
||||
/// <returns>Extracted and structured text from the image</returns>
|
||||
public async Task<string?> 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<string>> ConvertPdfToJpgAsync(string pdfPath, string outputFolder)
|
||||
{
|
||||
_pdfiumBootstrapper.EnsurePdfiumLoaded();
|
||||
|
||||
var imagePaths = new List<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<nop-label asp-for="NetWeight" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<nop-editor contenteditable="@Model.IsMeasurable" asp-for="NetWeight" />
|
||||
<nop-editor readonly="@Model.IsMeasurable" asp-for="NetWeight" />
|
||||
<span asp-validation-for="NetWeight"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -46,5 +46,24 @@
|
|||
<span asp-validation-for="Tare"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<nop-label asp-for="AverageWeight" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<nop-editor asp-for="AverageWeight" />
|
||||
<span asp-validation-for="AverageWeight"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-md-3">
|
||||
<nop-label asp-for="AverageWeightTreshold" />
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<nop-editor asp-for="AverageWeightTreshold" />
|
||||
<span asp-validation-for="AverageWeightTreshold"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Reference in New Issue