SPA test
This commit is contained in:
parent
e7e092cadc
commit
24319f5213
|
|
@ -43,11 +43,13 @@ It's written in C# running on .Net Core that is full cross-platform framework, t
|
||||||
```sh
|
```sh
|
||||||
PS D:\> git clone https://github.com/SciSharp/BotSharp-UI
|
PS D:\> git clone https://github.com/SciSharp/BotSharp-UI
|
||||||
PS D:\> cd BotSharp-UI
|
PS D:\> cd BotSharp-UI
|
||||||
PS D:\> npm install --force
|
PS D:\> npm install
|
||||||
PS D:\> npm run dev
|
PS D:\> npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Access http://localhost:5015/
|
Access http://localhost:5015/
|
||||||
|
|
||||||
|
[Online Demo with UI](https://botsharp.azurewebsites.net/)
|
||||||
|
|
||||||
<img src="./docs/static/screenshots/agent-builder-agents.png" height="450px"/>
|
<img src="./docs/static/screenshots/agent-builder-agents.png" height="450px"/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Text.Json" Version="8.0.0" />
|
<PackageReference Include="System.Text.Json" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="6.0.26" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,55 @@
|
||||||
using BotSharp.Abstraction.Messaging.JsonConverters;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace BotSharp.OpenAPI;
|
namespace BotSharp.OpenAPI;
|
||||||
|
|
||||||
public static class BotSharpOpenApiExtensions
|
public static class BotSharpOpenApiExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddBotSharpOpenAPI(this IServiceCollection services, IConfiguration config)
|
/// <summary>
|
||||||
|
/// Use Swagger/OpenAPI
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"></param>
|
||||||
|
/// <param name="config"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static IApplicationBuilder UseBotSharpOpenAPI(this IApplicationBuilder app, bool isDevelopment = false)
|
||||||
{
|
{
|
||||||
// Add services to the container.
|
if (app == null)
|
||||||
services.AddControllers()
|
{
|
||||||
.AddJsonOptions(options =>
|
throw new ArgumentNullException(nameof(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseSwagger();
|
||||||
|
if (isDevelopment)
|
||||||
|
{
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host BotSharp UI built in adapter-static
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="app"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
|
public static IApplicationBuilder UseBotSharpUI(this IApplicationBuilder app, bool isDevelopment = false)
|
||||||
|
{
|
||||||
|
if (app == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
// app.UseFileServer();
|
||||||
|
app.UseDefaultFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseSpa(config =>
|
||||||
|
{
|
||||||
|
if (isDevelopment)
|
||||||
{
|
{
|
||||||
options.JsonSerializerOptions.Converters.Add(new RichContentJsonConverter());
|
config.UseProxyToSpaDevelopmentServer("http://localhost:5015");
|
||||||
options.JsonSerializerOptions.Converters.Add(new TemplateMessageJsonConverter());
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
return app;
|
||||||
services.AddEndpointsApiExplorer();
|
|
||||||
services.AddSwaggerGen();
|
|
||||||
|
|
||||||
services.AddHttpContextAccessor();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using BotSharp.Abstraction.Loggers;
|
||||||
using BotSharp.Plugin.ChatHub.Hooks;
|
using BotSharp.Plugin.ChatHub.Hooks;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
|
@ -16,5 +17,6 @@ public class ChatHubPlugin : IBotSharpPlugin
|
||||||
{
|
{
|
||||||
// Register hooks
|
// Register hooks
|
||||||
services.AddScoped<IConversationHook, ChatHubConversationHook>();
|
services.AddScoped<IConversationHook, ChatHubConversationHook>();
|
||||||
|
services.AddScoped<IContentGeneratingHook, StreamingLogHook>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
using BotSharp.Abstraction.Messaging.JsonConverters;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace BotSharp.Plugin.ChatHub;
|
||||||
|
|
||||||
|
public static class ChatHubServiceExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add Swagger/OpenAPI
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services"></param>
|
||||||
|
/// <param name="config"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static IServiceCollection AddBotSharpOpenAPI(this IServiceCollection services, IConfiguration config)
|
||||||
|
{
|
||||||
|
// Add services to the container.
|
||||||
|
services.AddControllers()
|
||||||
|
.AddJsonOptions(options =>
|
||||||
|
{
|
||||||
|
options.JsonSerializerOptions.Converters.Add(new RichContentJsonConverter());
|
||||||
|
options.JsonSerializerOptions.Converters.Add(new TemplateMessageJsonConverter());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
services.AddSwaggerGen();
|
||||||
|
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Host BotSharp UI built in adapter-static
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="app"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
|
public static IApplicationBuilder UseChatHub(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
if (app == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(app));
|
||||||
|
}
|
||||||
|
|
||||||
|
app.UseMiddleware<WebSocketsMiddleware>();
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using BotSharp.Abstraction.Agents.Models;
|
||||||
|
using BotSharp.Abstraction.Loggers;
|
||||||
|
using BotSharp.Abstraction.Users.Models;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace BotSharp.Plugin.ChatHub.Hooks;
|
||||||
|
|
||||||
|
public class StreamingLogHook : IContentGeneratingHook
|
||||||
|
{
|
||||||
|
private readonly ConversationSetting _convSettings;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly IHubContext<SignalRHub> _chatHub;
|
||||||
|
|
||||||
|
public StreamingLogHook(
|
||||||
|
ConversationSetting convSettings,
|
||||||
|
IServiceProvider serivces,
|
||||||
|
IHubContext<SignalRHub> chatHub)
|
||||||
|
{
|
||||||
|
_convSettings = convSettings;
|
||||||
|
_services = serivces;
|
||||||
|
_chatHub = chatHub;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task BeforeGenerating(Agent agent, List<RoleDialogModel> conversations)
|
||||||
|
{
|
||||||
|
if (!_convSettings.ShowVerboseLog) return;
|
||||||
|
|
||||||
|
var user = _services.GetRequiredService<IUserIdentity>();
|
||||||
|
var dialog = conversations.Last();
|
||||||
|
var log = $"{dialog.Role}: {dialog.Content} [msg_id: {dialog.MessageId}] ==>";
|
||||||
|
await _chatHub.Clients.User(user.Id).SendAsync("OnContentLogGenerated", log);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AfterGenerated(RoleDialogModel message, TokenStatsModel tokenStats)
|
||||||
|
{
|
||||||
|
if (!_convSettings.ShowVerboseLog) return;
|
||||||
|
|
||||||
|
var agentService = _services.GetRequiredService<IAgentService>();
|
||||||
|
var agent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||||
|
|
||||||
|
var log = message.Role == AgentRole.Function ?
|
||||||
|
$"[{agent?.Name}]: {message.FunctionName}({message.FunctionArgs})" :
|
||||||
|
$"[{agent?.Name}]: {message.Content}" + $" <== [msg_id: {message.MessageId}]";
|
||||||
|
|
||||||
|
var user = _services.GetRequiredService<IUserIdentity>();
|
||||||
|
await _chatHub.Clients.User(user.Id).SendAsync("OnContentLogGenerated", tokenStats.Prompt);
|
||||||
|
await _chatHub.Clients.User(user.Id).SendAsync("OnContentLogGenerated", log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,4 +49,9 @@ public class SignalRHub : Hub
|
||||||
|
|
||||||
await base.OnConnectedAsync();
|
await base.OnConnectedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task PushEventLog(string message)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using BotSharp.Abstraction.Functions;
|
using BotSharp.Abstraction.Functions;
|
||||||
using BotSharp.Abstraction.Routing.Models;
|
|
||||||
using BotSharp.Plugin.KnowledgeBase.LlmContexts;
|
using BotSharp.Plugin.KnowledgeBase.LlmContexts;
|
||||||
|
|
||||||
namespace BotSharp.Plugin.KnowledgeBase.Functions;
|
namespace BotSharp.Plugin.KnowledgeBase.Functions;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
using BotSharp.Plugin.WebDriver.Services;
|
||||||
|
|
||||||
|
namespace BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
|
||||||
|
|
||||||
|
public partial class PlaywrightWebDriver
|
||||||
|
{
|
||||||
|
public async Task ChangeListValue(Agent agent, BrowsingContextIn context, string messageId)
|
||||||
|
{
|
||||||
|
// Retrieve the page raw html and infer the element path
|
||||||
|
var body = await _instance.Page.QuerySelectorAsync("body");
|
||||||
|
|
||||||
|
var str = new List<string>();
|
||||||
|
var inputs = await body.QuerySelectorAllAsync("input");
|
||||||
|
foreach (var input in inputs)
|
||||||
|
{
|
||||||
|
var text = await input.TextContentAsync();
|
||||||
|
var name = await input.GetAttributeAsync("name");
|
||||||
|
var type = await input.GetAttributeAsync("type");
|
||||||
|
str.Add($"<input name='{name}' type='{type}'>{text}</input>");
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs = await body.QuerySelectorAllAsync("textarea");
|
||||||
|
foreach (var input in inputs)
|
||||||
|
{
|
||||||
|
var text = await input.TextContentAsync();
|
||||||
|
var name = await input.GetAttributeAsync("name");
|
||||||
|
var type = await input.GetAttributeAsync("type");
|
||||||
|
str.Add($"<textarea name='{name}' type='{type}'>{text}</textarea>");
|
||||||
|
}
|
||||||
|
|
||||||
|
var driverService = _services.GetRequiredService<WebDriverService>();
|
||||||
|
var htmlElementContextOut = await driverService.LocateElement(agent, string.Join("", str), context.ElementName, messageId);
|
||||||
|
|
||||||
|
if (htmlElementContextOut.Index < 0)
|
||||||
|
{
|
||||||
|
throw new Exception($"Can't locate the web element {context.ElementName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var element = _instance.Page.Locator(htmlElementContextOut.TagName).Nth(htmlElementContextOut.Index);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await element.FillAsync(context.InputText);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception(ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using BotSharp.Abstraction.Agents;
|
||||||
|
using BotSharp.Plugin.WebDriver.Drivers.PlaywrightDriver;
|
||||||
|
|
||||||
|
namespace BotSharp.Plugin.WebDriver.Functions;
|
||||||
|
|
||||||
|
public class ChangeListValueFn : IFunctionCallback
|
||||||
|
{
|
||||||
|
public string Name => "change_list_value";
|
||||||
|
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly PlaywrightWebDriver _driver;
|
||||||
|
|
||||||
|
public ChangeListValueFn(IServiceProvider services,
|
||||||
|
PlaywrightWebDriver driver)
|
||||||
|
{
|
||||||
|
_services = services;
|
||||||
|
_driver = driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> Execute(RoleDialogModel message)
|
||||||
|
{
|
||||||
|
var args = JsonSerializer.Deserialize<BrowsingContextIn>(message.FunctionArgs);
|
||||||
|
|
||||||
|
var agentService = _services.GetRequiredService<IAgentService>();
|
||||||
|
var agent = await agentService.LoadAgent(message.CurrentAgentId);
|
||||||
|
await _driver.ChangeListValue(agent, args, message.MessageId);
|
||||||
|
|
||||||
|
message.Content = "Update successfully.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,9 @@ public class BrowsingContextIn
|
||||||
[JsonPropertyName("input_text")]
|
[JsonPropertyName("input_text")]
|
||||||
public string? InputText { get; set; }
|
public string? InputText { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("update_value")]
|
||||||
|
public string? UpdateValue { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("password")]
|
[JsonPropertyName("password")]
|
||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,17 @@ using Serilog;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
var loggerConfig = new LoggerConfiguration();
|
||||||
|
Log.Logger = loggerConfig
|
||||||
|
#if DEBUG
|
||||||
.MinimumLevel.Debug()
|
.MinimumLevel.Debug()
|
||||||
|
#else
|
||||||
|
.MinimumLevel.Warning()
|
||||||
|
#endif
|
||||||
.WriteTo.Console()
|
.WriteTo.Console()
|
||||||
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
|
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
builder.Host.UseSerilog();
|
||||||
builder.Host.UseSerilog(Log.Logger);
|
|
||||||
|
|
||||||
builder.Services.AddScoped<IUserIdentity, UserIdentity>();
|
builder.Services.AddScoped<IUserIdentity, UserIdentity>();
|
||||||
// Add bearer authentication
|
// Add bearer authentication
|
||||||
|
|
@ -48,7 +52,8 @@ builder.Services.AddBotSharpLogger(builder.Configuration);
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("MyCorsPolicy",
|
options.AddPolicy("MyCorsPolicy",
|
||||||
builder => builder.WithOrigins("http://localhost:5015",
|
builder => builder.WithOrigins("http://localhost:5015",
|
||||||
|
"http://localhost:5500",
|
||||||
"https://botsharp.scisharpstack.org",
|
"https://botsharp.scisharpstack.org",
|
||||||
"https://chat.scisharpstack.org")
|
"https://chat.scisharpstack.org")
|
||||||
.AllowAnyMethod()
|
.AllowAnyMethod()
|
||||||
|
|
@ -61,14 +66,8 @@ builder.Services.AddSignalR();
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
app.UseSwagger();
|
|
||||||
if (app.Environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.MapHub<SignalRHub>("/chatHub");
|
app.MapHub<SignalRHub>("/chatHub");
|
||||||
app.UseMiddleware<WebSocketsMiddleware>();
|
app.UseChatHub();
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
@ -77,12 +76,9 @@ app.MapControllers();
|
||||||
|
|
||||||
// Use BotSharp
|
// Use BotSharp
|
||||||
app.UseBotSharp();
|
app.UseBotSharp();
|
||||||
|
app.UseBotSharpOpenAPI();
|
||||||
|
app.UseBotSharpUI();
|
||||||
|
|
||||||
app.UseCors("MyCorsPolicy");
|
app.UseCors("MyCorsPolicy");
|
||||||
|
|
||||||
// Host BotSharp UI built in adapter-static
|
|
||||||
app.UseFileServer();
|
|
||||||
app.UseDefaultFiles();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
||||||
|
|
@ -29,18 +29,15 @@
|
||||||
<EmbeddedResource Remove="logs\**" />
|
<EmbeddedResource Remove="logs\**" />
|
||||||
<None Remove="logs\**" />
|
<None Remove="logs\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" />
|
<ProjectReference Include="..\..\tests\BotSharp.Plugin.PizzaBot\BotSharp.Plugin.PizzaBot.csproj" />
|
||||||
|
<ProjectReference Include="..\Plugins\BotSharp.Plugin.WebDriver\BotSharp.Plugin.WebDriver.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="$(SolutionName)==BotSharp">
|
<ItemGroup Condition="$(SolutionName)==BotSharp">
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,8 @@
|
||||||
"BotSharp.Plugin.Qdrant",
|
"BotSharp.Plugin.Qdrant",
|
||||||
"BotSharp.Plugin.ChatHub",
|
"BotSharp.Plugin.ChatHub",
|
||||||
"BotSharp.Plugin.WeChat",
|
"BotSharp.Plugin.WeChat",
|
||||||
"BotSharp.Plugin.PizzaBot"
|
"BotSharp.Plugin.PizzaBot",
|
||||||
|
"BotSharp.Plugin.WebDriver"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue