This commit is contained in:
Adam 2025-08-29 13:35:34 +02:00
parent 4b2cd32870
commit e03a9cf6db
22 changed files with 674 additions and 0 deletions

View File

@ -0,0 +1,164 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Nop.Plugin.Misc.AIPlugin.Components;
using Nop.Services.Cms;
using Nop.Services.Configuration;
using Nop.Services.Localization;
using Nop.Services.Plugins;
using Nop.Services.Security;
using Nop.Web.Framework.Infrastructure;
using Nop.Web.Framework.Menu;
namespace Nop.Plugin.Misc.AIPlugin
{
/// <summary>
/// Main plugin class
/// </summary>
public class AIPlugin : BasePlugin, IWidgetPlugin
{
protected readonly IActionContextAccessor _actionContextAccessor;
private readonly ISettingService _settingService;
//private readonly IWebHelper _webHelper;
protected readonly IPermissionService _permissionService;
protected readonly ILocalizationService _localizationService;
protected readonly IUrlHelperFactory _urlHelperFactory;
private readonly IAdminMenu _adminMenu;
//handle AdminMenuCreatedEvent
public AIPlugin(IActionContextAccessor actionContextAccessor,
ISettingService settingService,
//IWebHelper webHelper,
ILocalizationService localizationService,
IPermissionService permissionService,
IUrlHelperFactory urlHelperFactory,
IAdminMenu adminMenu)
{
_actionContextAccessor = actionContextAccessor;
_settingService = settingService;
//_webHelper = webHelper;
_localizationService = localizationService;
_urlHelperFactory = urlHelperFactory;
_adminMenu = adminMenu;
_permissionService = permissionService;
}
// --- INSTALL ---
public override async Task InstallAsync()
{
// Default settings
var settings = new OpenAiSettings
{
ApiKey = string.Empty
};
await _settingService.SaveSettingAsync(settings);
await base.InstallAsync();
}
// --- UNINSTALL ---
public override async Task UninstallAsync()
{
await _settingService.DeleteSettingAsync<OpenAiSettings>();
await base.UninstallAsync();
}
// --- WIDGETS ---
public bool HideInWidgetList => false;
public Task<IList<string>> GetWidgetZonesAsync()
{
return Task.FromResult<IList<string>>(new List<string> { PublicWidgetZones.ProductBoxAddinfoBefore, PublicWidgetZones.ProductDetailsBottom });
}
//public string GetWidgetViewComponentName(string widgetZone)
//{
// return "ProductAIWidget"; // A ViewComponent neve
//}
// --- ADMIN MENÜ ---
//public async Task ManageSiteMapAsync(AdminMenuItem rootNode)
//{
// if (!await _permissionService.AuthorizeAsync(StandardPermission.Configuration.MANAGE_PLUGINS))
// return;
// var pluginNode = new AdminMenuItem
// {
// SystemName = "AIPlugin.Configure",
// Title = "AI Assistant",
// Url = $"{_webHelper.GetStoreLocation()}Admin/AIPluginAdmin/Configure",
// Visible = true
// };
// rootNode.ChildNodes.Add(pluginNode);
// //return Task.CompletedTask;
//}
public async Task ManageSiteMapAsync(AdminMenuItem rootNode)
{
if (!await _permissionService.AuthorizeAsync(StandardPermission.Configuration.MANAGE_PLUGINS))
return;
var configurationItem = rootNode.ChildNodes.FirstOrDefault(node => node.SystemName.Equals("Configuration"));
if (configurationItem is null)
return;
var shippingItem = configurationItem.ChildNodes.FirstOrDefault(node => node.SystemName.Equals("Shipping"));
var widgetsItem = configurationItem.ChildNodes.FirstOrDefault(node => node.SystemName.Equals("Widgets"));
if (shippingItem is null && widgetsItem is null)
return;
var index = shippingItem is not null ? configurationItem.ChildNodes.IndexOf(shippingItem) : -1;
if (index < 0)
index = widgetsItem is not null ? configurationItem.ChildNodes.IndexOf(widgetsItem) : -1;
if (index < 0)
return;
configurationItem.ChildNodes.Insert(index + 1, new AdminMenuItem
{
Visible = true,
SystemName = "AI plugins",
Title = await _localizationService.GetResourceAsync("Plugins.Misc.SignalRApi.Menu.AI"),
IconClass = "far fa-dot-circle",
ChildNodes = new List<AdminMenuItem>
{
new()
{
// SystemName = "AIPlugin.Configure",
// Title = "AI Assistant",
// Url = $"{_webHelper.GetStoreLocation()}Admin/AIPluginAdmin/Configure",
// Visible = true
Visible = true,
SystemName = PluginDescriptor.SystemName,
Title = PluginDescriptor.FriendlyName,
IconClass = "far fa-circle",
Url = _adminMenu.GetMenuItemUrl("AIPlugin", "Configure"),
//Url = "Admin/SignalRApi/Configure",
//ControllerName = "SignalRApi",
//ActionName = "Configure",
//RouteValues = new RouteValueDictionary { { "area", AreaNames.ADMIN } }
}
}
});
}
public override string GetConfigurationPageUrl()
{
return _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext).RouteUrl("Plugin.Misc.AIPlugin.Configure");
}
public Type GetWidgetViewComponent(string widgetZone)
{
if (widgetZone is null)
throw new ArgumentNullException(nameof(widgetZone));
var zones = GetWidgetZonesAsync().Result;
return zones.Any(widgetZone.Equals) ? typeof(ProductAIWidgetViewComponent) : null;
}
}
}

View File

@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Misc.AIPlugin.Areas.Admin.Models;
using Nop.Plugin.Misc.AIPlugin;
//using Nop.Plugin.Misc.AIPlugin;
using Nop.Services.Configuration;
using Nop.Web.Framework.Controllers;
using Nop.Services.Messages;
namespace Nop.Plugin.Misc.AIPlugin.Controllers
{
[Area("Admin")]
public class AIPluginAdminController : BasePluginController
{
private readonly INotificationService _notificationService;
private readonly ISettingService _settingService;
private readonly OpenAiSettings _settings;
public AIPluginAdminController(INotificationService notificationService, ISettingService settingService, OpenAiSettings settings)
{
_notificationService = notificationService;
_settingService = settingService;
_settings = settings;
}
[HttpGet]
public IActionResult Configure()
{
var model = new ConfigureModel
{
ApiKey = _settings.ApiKey
};
return View("~/Plugins/Misc.AIPlugin/Views/Configure/Configure.cshtml", model);
}
[HttpPost]
public async Task<IActionResult> Configure(ConfigureModel model)
{
_settings.ApiKey = model.ApiKey;
await _settingService.SaveSettingAsync(_settings);
_notificationService.SuccessNotification("Beállítások mentve.");
return RedirectToAction("Configure");
}
}
}

View File

@ -0,0 +1,16 @@
using Nop.Web.Framework.Mvc.ModelBinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.AIPlugin.Areas.Admin.Models
{
public record ConfigureModel
{
[NopResourceDisplayName("Plugins.AIPlugin.Fields.ApiKey")]
public string ApiKey { get; set; }
}
}

View File

@ -0,0 +1,22 @@
@model Nop.Plugin.Misc.AIPlugin.Models.ConfigureModel
@{
Layout = "_AdminLayout";
Html.SetActiveMenuItemSystemName("AiAssistant.Configure");
}
<div class="card">
<div class="card-header">
<h2>AI Assistant Plugin - Beállítások</h2>
</div>
<div class="card-body">
<form asp-controller="AiAssistantAdmin" asp-action="Configure" method="post">
<div class="form-group">
<label asp-for="ApiKey"></label>
<input asp-for="ApiKey" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Mentés</button>
</form>
</div>
</div>

View File

@ -0,0 +1,10 @@
@inherits Nop.Web.Framework.Mvc.Razor.NopRazorPage<TModel>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Nop.Web.Framework
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using Nop.Web.Framework.UI
@using Nop.Web.Framework.Extensions
@using System.Text.Encodings.Web
@using Nop.Services.Events
@using Nop.Web.Framework.Events

View File

@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Web.Framework.Components;
namespace Nop.Plugin.Misc.AIPlugin.Components;
[ViewComponent(Name = "Custom")]
public class CustomViewComponent : NopViewComponent
{
public CustomViewComponent()
{
}
public IViewComponentResult Invoke(int productId)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Mvc;
using Nop.Web.Framework.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nop.Plugin.Misc.AIPlugin.Components
{
[ViewComponent(Name = "ProductAIWidget")]
public class ProductAIWidgetViewComponent : NopViewComponent
{
public IViewComponentResult Invoke(string widgetZone, object additionalData)
{
if(additionalData is Nop.Web.Models.Catalog.ProductOverviewModel)
{
var product = additionalData as Nop.Web.Models.Catalog.ProductOverviewModel;
if (product == null)
return Content(""); // ne rendereljen semmit, ha nincs product
return View("~/Plugins/Misc.AIPlugin/Views/ProductAIListWidget.cshtml", product);
}
else if (additionalData is Nop.Web.Models.Catalog.ProductDetailsModel)
{
var product = additionalData as Nop.Web.Models.Catalog.ProductDetailsModel;
if (product == null)
return Content(""); // ne rendereljen semmit, ha nincs product
return View("~/Plugins/Misc.AIPlugin/Views/ProductAIWidget.cshtml", product);
}
else {
return Content(""); // ne rendereljen semmit, ha nem productDetailModel vagy productOverviewModel
}
}
}
}

View File

@ -0,0 +1,8 @@
using Nop.Core;
namespace Nop.Plugin.Misc.AIPlugin.Domains;
public partial class CustomTable : BaseEntity
{
}

View File

@ -0,0 +1,39 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Nop.Core.Infrastructure;
namespace Nop.Plugin.Misc.AIPlugin.Infrastructure;
public class PluginNopStartup : INopStartup
{
/// <summary>
/// Add and configure any of the middleware
/// </summary>
/// <param name="services">Collection of service descriptors</param>
/// <param name="configuration">Configuration of the application</param>
public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
{
services.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new ViewLocationExpander());
});
//register services and interfaces
//services.AddScoped<CustomModelFactory, ICustomerModelFactory>();
}
/// <summary>
/// Configure the using of added middleware
/// </summary>
/// <param name="application">Builder for configuring an application's request pipeline</param>
public void Configure(IApplicationBuilder application)
{
}
/// <summary>
/// Gets order of this startup configuration implementation
/// </summary>
public int Order => 3000;
}

View File

@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Routing;
using Nop.Web.Framework.Mvc.Routing;
namespace Nop.Plugin.Misc.AIPlugin.Infrastructure;
/// <summary>
/// Represents plugin route provider
/// </summary>
public class RouteProvider : IRouteProvider
{
/// <summary>
/// Register routes
/// </summary>
/// <param name="endpointRouteBuilder">Route builder</param>
public void RegisterRoutes(IEndpointRouteBuilder endpointRouteBuilder)
{
}
/// <summary>
/// Gets a priority of route provider
/// </summary>
public int Priority => 0;
}

View File

@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Mvc.Razor;
namespace Nop.Plugin.Misc.AIPlugin.Infrastructure;
public class ViewLocationExpander : IViewLocationExpander
{
/// <summary>
/// Invoked by a <see cref="T:Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine" /> to determine the values that would be consumed by this instance
/// of <see cref="T:Microsoft.AspNetCore.Mvc.Razor.IViewLocationExpander" />. The calculated values are used to determine if the view location
/// has changed since the last time it was located.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Razor.ViewLocationExpanderContext" /> for the current view location
/// expansion operation.</param>
public void PopulateValues(ViewLocationExpanderContext context)
{
}
/// <summary>
/// Invoked by a <see cref="T:Microsoft.AspNetCore.Mvc.Razor.RazorViewEngine" /> to determine potential locations for a view.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Razor.ViewLocationExpanderContext" /> for the current view location
/// expansion operation.</param>
/// <param name="viewLocations">The sequence of view locations to expand.</param>
/// <returns>A list of expanded view locations.</returns>
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
if (context.AreaName == "Admin")
{
viewLocations = new[] { $"/Plugins/Nop.Plugin.Misc.AIPlugin/Areas/Admin/Views/{context.ControllerName}/{context.ViewName}.cshtml" }.Concat(viewLocations);
}
else
{
viewLocations = new[] { $"/Plugins/Nop.Plugin.Misc.AIPlugin/Views/{context.ControllerName}/{context.ViewName}.cshtml" }.Concat(viewLocations);
}
return viewLocations;
}
}

View File

@ -0,0 +1,20 @@
using FluentMigrator.Builders.Create.Table;
using Nop.Data.Mapping.Builders;
using Nop.Plugin.Misc.AIPlugin.Domains;
namespace Nop.Plugin.Misc.AIPlugin.Mapping.Builders;
public class PluginBuilder : NopEntityBuilder<CustomTable>
{
#region Methods
/// <summary>
/// Apply entity configuration
/// </summary>
/// <param name="table">Create table expression builder</param>
public override void MapEntity(CreateTableExpressionBuilder table)
{
}
#endregion
}

View File

@ -0,0 +1,16 @@
using Nop.Data.Mapping;
namespace Nop.Plugin.Misc.AIPlugin.Mapping;
public partial class NameCompatibility : INameCompatibility
{
/// <summary>
/// Gets table name for mapping with the type
/// </summary>
public Dictionary<Type, string> TableNames => new();
/// <summary>
/// Gets column name for mapping with the entity's property and type
/// </summary>
public Dictionary<(Type, string), string> ColumnName => new();
}

View File

@ -0,0 +1,19 @@
using FluentMigrator;
using Nop.Data.Extensions;
using Nop.Data.Migrations;
using Nop.Plugin.Misc.AIPlugin.Domains;
namespace Nop.Plugin.Misc.AIPlugin.Migrations;
//2024-10-31
[NopMigration("2025-08-29 12:07:22", "Nop.Plugin.Misc.AIPlugin schema", MigrationProcessType.Installation)]
public class SchemaMigration : AutoReversingMigration
{
/// <summary>
/// Collect the UP migration expressions
/// </summary>
public override void Up()
{
Create.TableFor<CustomTable>();
}
}

View File

@ -0,0 +1,73 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<OutputPath>$(SolutionDir)\Presentation\Nop.Web\Plugins\Misc.AIPlugin</OutputPath>
<OutDir>$(OutputPath)</OutDir>
<!--Set this parameter to true to get the dlls copied from the NuGet cache to the output of your project.
You need to set this parameter to true if your plugin has a nuget package
to ensure that the dlls copied from the NuGet cache to the output of your project-->
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<None Remove="logo.jpg" />
<None Remove="plugin.json" />
<None Remove="Views\_ViewImports.cshtml" />
<None Remove="Areas\Admin\Views\_ViewImports.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="logo.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="plugin.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Areas\Admin\Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Areas\Admin\Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="Views\_ViewImports.cshtml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(SolutionDir)\Presentation\Nop.Web\Nop.Web.csproj" />
<ClearPluginAssemblies Include="$(SolutionDir)\Build\ClearPluginAssemblies.proj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Admin\Components\" />
<Folder Include="Areas\Admin\Extensions\" />
<Folder Include="Areas\Admin\Factories\" />
<Folder Include="Areas\Admin\Validators\" />
<Folder Include="Controllers\" />
<Folder Include="Extensions\" />
<Folder Include="Factories\" />
<Folder Include="Models\" />
<Folder Include="Validators\" />
</ItemGroup>
<ItemGroup>
<None Update="Views\ProductAIListWidget.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Views\ProductAIWidget.cshtml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<!-- This target execute after "Build" target -->
<Target Name="NopTarget" AfterTargets="Build">
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
</Target>
</Project>

View File

@ -0,0 +1,10 @@
using global::Nop.Core.Configuration;
namespace Nop.Plugin.Misc.AIPlugin
{
public class OpenAiSettings : ISettings
{
public string ApiKey { get; set; }
}
}

View File

@ -0,0 +1,45 @@
using Nop.Plugin.Misc.AIPlugin;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Nop.Services.Configuration;
namespace Nop.Plugin.Misc.AIPlugin.Services
{
public class OpenAiService
{
private readonly OpenAiSettings _settings;
public OpenAiService(OpenAiSettings settings)
{
_settings = settings;
}
public async Task<string> AskAsync(string prompt)
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _settings.ApiKey);
var body = new
{
model = "gpt-4o-mini",
messages = new[]
{
new { role = "system", content = "You are a helpful assistant." },
new { role = "user", content = prompt }
}
};
var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://api.openai.com/v1/chat/completions", content);
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
return doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString();
}
}
}

View File

@ -0,0 +1,20 @@
@model Nop.Web.Models.Catalog.ProductOverviewModel
@if (Model != null)
{
<div class="ai-question-box">
<h5>Kérdezz a termékről!</h5>
<textarea id="ai-question"></textarea>
<button id="ask-ai-btn" class="btn btn-primary">Kérdezz</button>
<div id="ai-answer"></div>
</div>
<script>
$("#ask-ai-btn").click(function() {
$.post("/AiAssistant/Ask", { productId: "@Model.Id", question: $("#ai-question").val() }, function (data) {
$("#ai-answer").html(data);
});
});
</script>
}

View File

@ -0,0 +1,20 @@
@model Nop.Web.Models.Catalog.ProductDetailsModel
@if (Model != null)
{
<div class="ai-question-box">
<h5>Kérdezz a termékről!</h5>
<textarea id="ai-question"></textarea>
<button id="ask-ai-btn" class="btn btn-primary">Kérdezz</button>
<div id="ai-answer"></div>
</div>
<script>
$("#ask-ai-btn").click(function() {
$.post("/AiAssistant/Ask", { productId: "@Model.Id", question: $("#ai-question").val() }, function (data) {
$("#ai-answer").html(data);
});
});
</script>
}

View File

@ -0,0 +1,15 @@
@inherits Nop.Web.Framework.Mvc.Razor.NopRazorPage<TModel>
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Nop.Web.Framework
@using Microsoft.AspNetCore.Mvc.ViewFeatures
@using Nop.Web.Framework.UI
@using Nop.Web.Framework.Extensions
@using System.Text.Encodings.Web
@using Nop.Services.Events
@using Nop.Web.Framework.Events
@using Nop.Web.Framework.Infrastructure
@using Nop.Plugin.Misc.AIPlugin
@* @using Nop.Plugin.Misc.AIPlugin.Models *@
@using Nop.Plugin.Misc.AIPlugin.Services
@* @using Nop.Plugin.Misc.AIPlugin.Hubs *@

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,13 @@
{
"Group": "Misc",
"FriendlyName": "AIPlugin",
"SystemName": "Misc.AIPlugin",
"Version": "1.00",
"SupportedVersions": [
"4.80"
],
"Author": "Adam Gelencser",
"DisplayOrder": 1,
"FileName": "Nop.Plugin.Misc.AIPlugin.dll",
"Description": ""
}