| @@ -0,0 +1,11 @@ | |||
| using LLama.Web.Models; | |||
| namespace LLama.Web.Hubs | |||
| { | |||
| public interface IInteractiveClient | |||
| { | |||
| Task OnStatus(string status); | |||
| Task OnResponse(ResponseFragment fragment); | |||
| Task OnError(string error); | |||
| } | |||
| } | |||
| @@ -0,0 +1,106 @@ | |||
| using LLama.Web.Models; | |||
| using Microsoft.AspNetCore.SignalR; | |||
| using Microsoft.Extensions.Options; | |||
| using System.Collections.Concurrent; | |||
| using System.Diagnostics; | |||
| namespace LLama.Web.Hubs | |||
| { | |||
| public class InteractiveHub : Hub<IInteractiveClient> | |||
| { | |||
| //TODO: Move all this out to ModelSessionService | |||
| private static readonly ConcurrentDictionary<string, ModelSession> _modelSessions = new ConcurrentDictionary<string, ModelSession>(); | |||
| private readonly ILogger<InteractiveHub> _logger; | |||
| private readonly LLamaOptions _options; | |||
| public InteractiveHub(ILogger<InteractiveHub> logger, IOptions<LLamaOptions> options) | |||
| { | |||
| _logger = logger; | |||
| _options = options.Value; | |||
| } | |||
| public override async Task OnConnectedAsync() | |||
| { | |||
| _logger.Log(LogLevel.Information, "OnConnectedAsync, Id: {0}", Context.ConnectionId); | |||
| await base.OnConnectedAsync(); | |||
| await Clients.Caller.OnStatus("Connected"); | |||
| } | |||
| public override async Task OnDisconnectedAsync(Exception? exception) | |||
| { | |||
| _logger.Log(LogLevel.Information, "[OnDisconnectedAsync], Id: {0}", Context.ConnectionId); | |||
| if (_modelSessions.TryRemove(Context.ConnectionId, out var modelSession)) | |||
| { | |||
| _logger.Log(LogLevel.Information, "[OnDisconnectedAsync] - Removed InteractiveExecutor, Id: {0}", Context.ConnectionId); | |||
| modelSession.Dispose(); | |||
| } | |||
| await base.OnDisconnectedAsync(exception); | |||
| } | |||
| [HubMethodName("LoadModel")] | |||
| public async Task OnLoadModel(string modelName, string promptName, string parameterName) | |||
| { | |||
| _logger.Log(LogLevel.Information, "[OnLoadModel] - Load new model, Connection: {0}, Model: {1}, Prompt: {2}, Parameter: {3}", Context.ConnectionId, modelName, promptName, parameterName); | |||
| if (_modelSessions.TryRemove(Context.ConnectionId, out var modelSession)) | |||
| { | |||
| _logger.Log(LogLevel.Information, "[OnLoadModel] - Removed existing model session, Id: {0}", Context.ConnectionId); | |||
| modelSession.Dispose(); | |||
| } | |||
| var modelOption = _options.Models.First(x => x.Name == modelName); | |||
| var promptOption = _options.Prompts.First(x => x.Name == promptName); | |||
| var parameterOption = _options.Parameters.First(x => x.Name == parameterName); | |||
| modelSession = new ModelSession(Context.ConnectionId, modelOption, promptOption, parameterOption); | |||
| if (!_modelSessions.TryAdd(Context.ConnectionId, modelSession)) | |||
| { | |||
| _logger.Log(LogLevel.Error, "[OnLoadModel] - Failed to add new model session, Connection: {0}", Context.ConnectionId); | |||
| await Clients.Caller.OnError("No model has been loaded"); | |||
| return; | |||
| } | |||
| _logger.Log(LogLevel.Information, "[OnLoadModel] - New model session added, Connection: {0}", Context.ConnectionId); | |||
| await Clients.Caller.OnStatus("Loaded"); | |||
| } | |||
| [HubMethodName("SendPrompt")] | |||
| public async Task OnSendPrompt(string prompt) | |||
| { | |||
| var stopwatch = Stopwatch.GetTimestamp(); | |||
| _logger.Log(LogLevel.Information, "[OnSendPrompt] - New prompt received, Connection: {0}", Context.ConnectionId); | |||
| if (!_modelSessions.TryGetValue(Context.ConnectionId, out var modelSession)) | |||
| { | |||
| _logger.Log(LogLevel.Warning, "[OnSendPrompt] - No model has been loaded for this connection, Connection: {0}", Context.ConnectionId); | |||
| await Clients.Caller.OnError("No model has been loaded"); | |||
| return; | |||
| } | |||
| // Create unique response id | |||
| modelSession.ResponseId = Guid.NewGuid().ToString(); | |||
| // Send begin of response | |||
| await Clients.Caller.OnResponse(new ResponseFragment(modelSession.ResponseId, isFirst: true)); | |||
| // Send content of response | |||
| await foreach (var fragment in modelSession.InferAsync(prompt, CancellationTokenSource.CreateLinkedTokenSource(Context.ConnectionAborted))) | |||
| { | |||
| await Clients.Caller.OnResponse(new ResponseFragment(modelSession.ResponseId, fragment)); | |||
| } | |||
| // Send end of response | |||
| var elapsedTime = Stopwatch.GetElapsedTime(stopwatch); | |||
| var signature = modelSession.IsInferCanceled() | |||
| ? $"Inference cancelled after {elapsedTime.TotalSeconds:F0} seconds" | |||
| : $"Inference completed in {elapsedTime.TotalSeconds:F0} seconds"; | |||
| await Clients.Caller.OnResponse(new ResponseFragment(modelSession.ResponseId, signature, isLast: true)); | |||
| _logger.Log(LogLevel.Information, "[OnSendPrompt] - Inference complete, Connection: {0}, Elapsed: {1}, Canceled: {2}", Context.ConnectionId, elapsedTime, modelSession.IsInferCanceled()); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | |||
| <PropertyGroup> | |||
| <TargetFramework>net7.0</TargetFramework> | |||
| <Nullable>disable</Nullable> | |||
| <ImplicitUsings>enable</ImplicitUsings> | |||
| </PropertyGroup> | |||
| <ItemGroup> | |||
| <ProjectReference Include="..\LLama\LLamaSharp.csproj" /> | |||
| </ItemGroup> | |||
| <ItemGroup> | |||
| <Folder Include="wwwroot\image\" /> | |||
| </ItemGroup> | |||
| </Project> | |||
| @@ -0,0 +1,20 @@ | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class LLamaOptions | |||
| { | |||
| public List<ModelOptions> Models { get; set; } | |||
| public List<PromptOptions> Prompts { get; set; } = new List<PromptOptions>(); | |||
| public List<ParameterOptions> Parameters { get; set; } = new List<ParameterOptions>(); | |||
| public void Initialize() | |||
| { | |||
| foreach (var prompt in Prompts) | |||
| { | |||
| if (File.Exists(prompt.Path)) | |||
| { | |||
| prompt.Prompt = File.ReadAllText(prompt.Path).Trim(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| using LLama.Common; | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class ModelOptions : ModelParams | |||
| { | |||
| public ModelOptions() : base("", 512, 20, 1337, true, true, false, false, "", "", -1, 512, false, false) | |||
| { | |||
| } | |||
| public string Name { get; set; } | |||
| public int MaxInstances { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,65 @@ | |||
| using LLama.Abstractions; | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class ModelSession : IDisposable | |||
| { | |||
| private bool _isFirstInteraction = true; | |||
| private ModelOptions _modelOptions; | |||
| private PromptOptions _promptOptions; | |||
| private ParameterOptions _inferenceOptions; | |||
| private ITextStreamTransform _outputTransform; | |||
| private InteractiveExecutor _interactiveExecutor; | |||
| private CancellationTokenSource _cancellationTokenSource; | |||
| public ModelSession(string connectionId, ModelOptions modelOptions, PromptOptions promptOptions, ParameterOptions parameterOptions) | |||
| { | |||
| ConnectionId = connectionId; | |||
| _modelOptions = modelOptions; | |||
| _promptOptions = promptOptions; | |||
| _inferenceOptions = parameterOptions; | |||
| _interactiveExecutor = new InteractiveExecutor(new LLamaModel(_modelOptions)); | |||
| _inferenceOptions.AntiPrompts = _promptOptions.AntiPrompt?.Concat(_inferenceOptions.AntiPrompts ?? Enumerable.Empty<string>()).Distinct() ?? _inferenceOptions.AntiPrompts; | |||
| if (_promptOptions.OutputFilter?.Count > 0) | |||
| _outputTransform = new LLamaTransforms.KeywordTextOutputStreamTransform(_promptOptions.OutputFilter, redundancyLength: 5); | |||
| } | |||
| public string ConnectionId { get; } | |||
| public string ResponseId { get; set; } | |||
| public IAsyncEnumerable<string> InferAsync(string message, CancellationTokenSource cancellationTokenSource) | |||
| { | |||
| _cancellationTokenSource = cancellationTokenSource; | |||
| if (_isFirstInteraction) | |||
| { | |||
| _isFirstInteraction = false; | |||
| message = _promptOptions.Prompt + message; | |||
| } | |||
| if (_outputTransform is not null) | |||
| return _outputTransform.TransformAsync(_interactiveExecutor.InferAsync(message, _inferenceOptions, _cancellationTokenSource.Token)); | |||
| return _interactiveExecutor.InferAsync(message, _inferenceOptions, _cancellationTokenSource.Token); | |||
| } | |||
| public void CancelInfer() | |||
| { | |||
| _cancellationTokenSource?.Cancel(); | |||
| } | |||
| public bool IsInferCanceled() | |||
| { | |||
| return _cancellationTokenSource.IsCancellationRequested; | |||
| } | |||
| public void Dispose() | |||
| { | |||
| _inferenceOptions = null; | |||
| _outputTransform = null; | |||
| _interactiveExecutor.Model?.Dispose(); | |||
| _interactiveExecutor = null; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| using LLama.Common; | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class ParameterOptions : InferenceParams | |||
| { | |||
| public string Name { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,11 @@ | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class PromptOptions | |||
| { | |||
| public string Name { get; set; } | |||
| public string Path { get; set; } | |||
| public string Prompt { get; set; } | |||
| public List<string> AntiPrompt { get; set; } | |||
| public List<string> OutputFilter { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| namespace LLama.Web.Models | |||
| { | |||
| public class ResponseFragment | |||
| { | |||
| public ResponseFragment(string id, string content = null, bool isFirst = false, bool isLast = false) | |||
| { | |||
| Id = id; | |||
| IsLast = isLast; | |||
| IsFirst = isFirst; | |||
| Content = content; | |||
| } | |||
| public string Id { get; set; } | |||
| public string Content { get; set; } | |||
| public bool IsLast { get; set; } | |||
| public bool IsFirst { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| @page | |||
| @model ErrorModel | |||
| @{ | |||
| ViewData["Title"] = "Error"; | |||
| } | |||
| <h1 class="text-danger">Error.</h1> | |||
| <h2 class="text-danger">An error occurred while processing your request.</h2> | |||
| @if (Model.ShowRequestId) | |||
| { | |||
| <p> | |||
| <strong>Request ID:</strong> <code>@Model.RequestId</code> | |||
| </p> | |||
| } | |||
| <h3>Development Mode</h3> | |||
| <p> | |||
| Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred. | |||
| </p> | |||
| <p> | |||
| <strong>The Development environment shouldn't be enabled for deployed applications.</strong> | |||
| It can result in displaying sensitive information from exceptions to end users. | |||
| For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong> | |||
| and restarting the app. | |||
| </p> | |||
| @@ -0,0 +1,27 @@ | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| using System.Diagnostics; | |||
| namespace LLama.Web.Pages | |||
| { | |||
| [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] | |||
| [IgnoreAntiforgeryToken] | |||
| public class ErrorModel : PageModel | |||
| { | |||
| public string? RequestId { get; set; } | |||
| public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); | |||
| private readonly ILogger<ErrorModel> _logger; | |||
| public ErrorModel(ILogger<ErrorModel> logger) | |||
| { | |||
| _logger = logger; | |||
| } | |||
| public void OnGet() | |||
| { | |||
| RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,10 @@ | |||
| @page | |||
| @model IndexModel | |||
| @{ | |||
| ViewData["Title"] = "Home page"; | |||
| } | |||
| <div class="text-center"> | |||
| <h1 class="display-4">Welcome</h1> | |||
| <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> | |||
| </div> | |||
| @@ -0,0 +1,20 @@ | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| namespace LLama.Web.Pages | |||
| { | |||
| public class IndexModel : PageModel | |||
| { | |||
| private readonly ILogger<IndexModel> _logger; | |||
| public IndexModel(ILogger<IndexModel> logger) | |||
| { | |||
| _logger = logger; | |||
| } | |||
| public void OnGet() | |||
| { | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,325 @@ | |||
| @page | |||
| @model InteractiveModel | |||
| @{ | |||
| } | |||
| <div class="d-flex flex-row h-100 pt-1 pb-1"> | |||
| <div class="d-flex flex-column h-100 border me-1 w-25 overflow-auto"> | |||
| <div class="d-flex flex-row justify-content-between border-bottom p-1 align-items-center"> | |||
| <h4>Interactive</h4> | |||
| <div> | |||
| <span>Hub: <b id="socket">Disconnected</b></span> | |||
| </div> | |||
| </div> | |||
| <div class="m-1"> | |||
| <small>Model</small> | |||
| <select id="Model" class="form-control form-select input-control" required="required" autocomplete="off"> | |||
| <option value="" disabled selected hidden>Please Select</option> | |||
| @foreach (var modelOption in Model.Options.Models) | |||
| { | |||
| <option value="@modelOption.Name">@modelOption.Name</option> | |||
| } | |||
| </select> | |||
| </div> | |||
| <div class="m-1"> | |||
| <small>Parameters</small> | |||
| <select id="Parameter" class="form-control form-select input-control" required="required" autocomplete="off"> | |||
| <option value="" disabled selected hidden>Please Select</option> | |||
| @foreach (var parameterOption in Model.Options.Parameters) | |||
| { | |||
| <option value="@parameterOption.Name">@parameterOption.Name</option> | |||
| } | |||
| </select> | |||
| </div> | |||
| <div class="m-1"> | |||
| <small>Prompt</small> | |||
| <select id="Prompt" class="form-control form-select input-control" required="required" autocomplete="off"> | |||
| <option value="" disabled selected hidden>Please Select</option> | |||
| @foreach (var promptOption in Model.Options.Prompts) | |||
| { | |||
| <option value="@promptOption.Name" data-prompt="@promptOption.Prompt">@promptOption.Name</option> | |||
| } | |||
| </select> | |||
| <textarea id="PromptText" class="form-control mt-1" rows="12" disabled="disabled" style="font-size:13px;resize:none"></textarea> | |||
| </div> | |||
| <div class="d-flex flex-grow-1"></div> | |||
| <div id="session-details" class="m-1"> | |||
| </div> | |||
| <div class="m-1"> | |||
| <button class="btn btn-outline-secondary input-control w-100" type="button" id="load">Create Session</button> | |||
| </div> | |||
| </div> | |||
| <div class="d-flex flex-column h-100 w-75"> | |||
| <div class="section-head"> | |||
| </div> | |||
| <div id="scroll-container" class="section-content border"> | |||
| <div id="output-container" class="d-flex flex-column gap-1 p-1"> | |||
| </div> | |||
| </div> | |||
| <div class="section-foot"> | |||
| <div class="input-group mt-2"> | |||
| <textarea id="input" type="text" class="form-control" value="what is a tree?" style="resize:none" rows="4">What is an apple?</textarea> | |||
| <div class="d-flex flex-column"> | |||
| <div class="d-flex flex-fill"> | |||
| <button class="btn btn-outline-secondary input-control w-100" type="button" id="send" disabled="disabled" autocomplete="off">Send Message</button> | |||
| </div> | |||
| <div class="d-flex"> | |||
| <button class="btn btn-outline-secondary w-100" type="button" id="cancel" disabled="disabled" autocomplete="off"> | |||
| <i class="bi-x-circle"></i> | |||
| </button> | |||
| <button class="btn btn-outline-secondary input-control w-100" type="button" id="clear" disabled="disabled" autocomplete="off"> | |||
| <i class="bi-trash3"></i> | |||
| </button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <script id="outputErrorTemplate" type="text/html"> | |||
| <i class="form-control text-danger">{{text}}</i> | |||
| </script> | |||
| <script id="outputInfoTemplate" type="text/html"> | |||
| <i class="form-control text-success">{{text}}</i> | |||
| </script> | |||
| <script id="outputUserTemplate" type="text/html"> | |||
| <div class="d-flex flex-row form-control bg-light"> | |||
| <div class="m-2 me-4"> | |||
| <img src="~/image/human.png" width="60"/> | |||
| </div> | |||
| <div class="d-flex flex-column flex-fill justify-content-between"> | |||
| <span class="w-100" style="resize:none" >{{text}}</span> | |||
| <div class="d-flex justify-content-end"> | |||
| <i>{{date}}</i> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </script> | |||
| <script id="outputBotTemplate" type="text/html"> | |||
| <div class="d-flex flex-row form-control"> | |||
| <div class="m-2 me-4"> | |||
| <img src="~/image/robot.png" width="60"/> | |||
| </div> | |||
| <div id="{{id}}" class="d-flex flex-column flex-fill justify-content-between"> | |||
| <span class="content"> | |||
| <img src="~/image/loading.gif" width="30" /> | |||
| </span> | |||
| <div class="d-flex justify-content-end"> | |||
| <div class="d-flex flex-column align-items-end"> | |||
| <i class="date"></i> | |||
| <i> | |||
| <small class="signature"></small> | |||
| </i> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </script> | |||
| <script id="sessionDetailsTemplate" type="text/html"> | |||
| <div> | |||
| <small>Session Details </small> | |||
| </div> | |||
| <div> | |||
| <i>Model: </i> | |||
| <span>{{model}}</span> | |||
| </div> | |||
| <div> | |||
| <i>Prompt: </i> | |||
| <span>{{prompt}}</span> | |||
| </div> | |||
| <div> | |||
| <i>Parameters: </i> | |||
| <span>{{parameter}}</span> | |||
| </div> | |||
| </script> | |||
| @section Scripts { | |||
| <script> | |||
| const outputErrorTemplate = $("#outputErrorTemplate").html(); | |||
| const outputInfoTemplate = $("#outputInfoTemplate").html(); | |||
| const outputUserTemplate = $("#outputUserTemplate").html(); | |||
| const outputBotTemplate = $("#outputBotTemplate").html(); | |||
| const sessionDetailsTemplate = $("#sessionDetailsTemplate").html(); | |||
| const connection = new signalR.HubConnectionBuilder().withUrl("/InteractiveHub").build(); | |||
| const scrollContainer = $("#scroll-container"); | |||
| const outputContainer = $("#output-container"); | |||
| const onStatus = (status) => { | |||
| if (status == "Connected") { | |||
| $("#socket").text("Connected").addClass("text-success"); | |||
| } | |||
| else if (status == "Loaded") { | |||
| enableControls(); | |||
| $("#session-details").html(Mustache.render(sessionDetailsTemplate, { model: getSelectedModel(), prompt: getSelectedPrompt(), parameter: getSelectedParameter() })); | |||
| onInfo(`New model session successfully started`) | |||
| } | |||
| } | |||
| const onError = (error) => { | |||
| enableControls(); | |||
| outputContainer.append(Mustache.render(outputErrorTemplate, { text: error, date: getDateTime() })); | |||
| } | |||
| const onInfo = (message) => { | |||
| outputContainer.append(Mustache.render(outputInfoTemplate, { text: message, date: getDateTime() })); | |||
| } | |||
| let responseContent; | |||
| let responseContainer; | |||
| let responseFirstFragment; | |||
| const onResponse = (response) => { | |||
| if (!response) | |||
| return; | |||
| if (response.isFirst) { | |||
| outputContainer.append(Mustache.render(outputBotTemplate, response)); | |||
| responseContainer = $(`#${response.id}`); | |||
| responseContent = responseContainer.find(".content"); | |||
| responseFirstFragment = true; | |||
| scrollToBottom(); | |||
| return; | |||
| } | |||
| if (response.isLast) { | |||
| enableControls(); | |||
| responseContainer.find(".signature").append(response.content); | |||
| scrollToBottom(); | |||
| } | |||
| else { | |||
| if (responseFirstFragment) { | |||
| responseContent.empty(); | |||
| responseFirstFragment = false; | |||
| responseContainer.find(".date").append(getDateTime()); | |||
| } | |||
| responseContent.append(response.content); | |||
| scrollToBottom(); | |||
| } | |||
| } | |||
| const sendPrompt = () => { | |||
| const text = $("#input").val(); | |||
| if (text) { | |||
| disableControls(); | |||
| outputContainer.append(Mustache.render(outputUserTemplate, { text: text, date: getDateTime() })); | |||
| connection.invoke('SendPrompt', text); | |||
| $("#input").val(null); | |||
| } | |||
| } | |||
| const cancelPrompt = () => { | |||
| //TODO: Need to cancel inference via http as signalr will synchronize requests | |||
| // waiting on ModelSessionService so we can locate an cancel ModelSessions outside the Hub | |||
| //connection.invoke('CancelPrompt'); | |||
| } | |||
| const loadModel = () => { | |||
| const modelName = getSelectedModel(); | |||
| const promptName = getSelectedPrompt(); | |||
| const parameterName = getSelectedParameter(); | |||
| if (!modelName || !promptName || !parameterName) { | |||
| onError("Please select a valid Model, Parameter and Prompt"); | |||
| return; | |||
| } | |||
| disableControls(); | |||
| connection.invoke('LoadModel', modelName, promptName, parameterName); | |||
| } | |||
| const enableControls = () => { | |||
| $(".input-control").removeAttr("disabled"); | |||
| } | |||
| const disableControls = () => { | |||
| $(".input-control").attr("disabled", "disabled"); | |||
| } | |||
| const clearOutput = () => { | |||
| outputContainer.empty(); | |||
| } | |||
| const updatePrompt = () => { | |||
| const customPrompt = $("#PromptText"); | |||
| const selection = $("option:selected", "#Prompt"); | |||
| const selectedValue = selection.data("prompt"); | |||
| customPrompt.text(selectedValue); | |||
| } | |||
| const getSelectedModel = () => { | |||
| return $("option:selected", "#Model").val(); | |||
| } | |||
| const getSelectedParameter = () => { | |||
| return $("option:selected", "#Parameter").val(); | |||
| } | |||
| const getSelectedPrompt = () => { | |||
| return $("option:selected", "#Prompt").val(); | |||
| } | |||
| const getDateTime = () => { | |||
| const dateTime = new Date(); | |||
| return dateTime.toLocaleString(); | |||
| } | |||
| const scrollToBottom = () => { | |||
| const scrollTop = scrollContainer.scrollTop(); | |||
| const scrollHeight = scrollContainer[0].scrollHeight; | |||
| if (scrollTop + 70 >= scrollHeight - scrollContainer.innerHeight()) { | |||
| scrollContainer.scrollTop(scrollContainer[0].scrollHeight) | |||
| } | |||
| } | |||
| // Map UI functions | |||
| $("#load").on("click", loadModel); | |||
| $("#send").on("click", sendPrompt); | |||
| $("#clear").on("click", clearOutput); | |||
| $("#cancel").on("click", cancelPrompt); | |||
| $("#Prompt").on("change", updatePrompt); | |||
| // Map signalr functions | |||
| connection.on("OnStatus", onStatus); | |||
| connection.on("OnError", onError); | |||
| connection.on("OnResponse", onResponse); | |||
| connection.start(); | |||
| </script> | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| using LLama.Web.Models; | |||
| using Microsoft.AspNetCore.Mvc; | |||
| using Microsoft.AspNetCore.Mvc.RazorPages; | |||
| using Microsoft.Extensions.Options; | |||
| namespace LLama.Web.Pages | |||
| { | |||
| public class InteractiveModel : PageModel | |||
| { | |||
| private readonly ILogger<InteractiveModel> _logger; | |||
| public InteractiveModel(ILogger<InteractiveModel> logger, IOptions<LLamaOptions> options) | |||
| { | |||
| _logger = logger; | |||
| Options = options.Value; | |||
| } | |||
| public LLamaOptions Options { get; set; } | |||
| public void OnGet() | |||
| { | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| .section-content { | |||
| flex: 1; | |||
| overflow-y: scroll; | |||
| } | |||
| @@ -0,0 +1,54 @@ | |||
| <!DOCTYPE html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8" /> | |||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
| <title>@ViewData["Title"] - LLama.Web</title> | |||
| <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> | |||
| <link href="~/lib/bootstrap/dist/css/bootstrap-icons.css" rel="stylesheet" /> | |||
| <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> | |||
| <link rel="stylesheet" href="~/LLama.Web.styles.css" asp-append-version="true" /> | |||
| </head> | |||
| <body> | |||
| <header> | |||
| <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow "> | |||
| <div class="container"> | |||
| <a class="navbar-brand" asp-area="" asp-page="/Index">LLama.Web</a> | |||
| <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent" | |||
| aria-expanded="false" aria-label="Toggle navigation"> | |||
| <span class="navbar-toggler-icon"></span> | |||
| </button> | |||
| <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> | |||
| <ul class="navbar-nav flex-grow-1"> | |||
| <li class="nav-item"> | |||
| <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> | |||
| </li> | |||
| <li class="nav-item"> | |||
| <a class="nav-link text-dark" asp-area="" asp-page="/Interactive">Interactive</a> | |||
| </li> | |||
| </ul> | |||
| </div> | |||
| </div> | |||
| </nav> | |||
| </header> | |||
| <main class="container" role="main" > | |||
| @RenderBody() | |||
| </main> | |||
| <footer class="border-top footer text-muted"> | |||
| <div class="container"> | |||
| © 2023 - LLama.Web | |||
| </div> | |||
| </footer> | |||
| <script src="~/lib/jquery/dist/jquery.min.js"></script> | |||
| <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> | |||
| <script src="~/lib/mustache/mustache.js"></script> | |||
| <script src="~/lib/signalr/signalr.min.js"></script> | |||
| <script src="~/js/site.js" asp-append-version="true"></script> | |||
| @await RenderSectionAsync("Scripts", required: false) | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,44 @@ | |||
| /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification | |||
| for details on configuring this project to bundle and minify static web assets. */ | |||
| a.navbar-brand { | |||
| white-space: normal; | |||
| text-align: center; | |||
| word-break: break-all; | |||
| } | |||
| a { | |||
| color: #0077cc; | |||
| } | |||
| .btn-primary { | |||
| color: #fff; | |||
| background-color: #1b6ec2; | |||
| border-color: #1861ac; | |||
| } | |||
| .nav-pills .nav-link.active, .nav-pills .show > .nav-link { | |||
| color: #fff; | |||
| background-color: #1b6ec2; | |||
| border-color: #1861ac; | |||
| } | |||
| .border-top { | |||
| border-top: 1px solid #e5e5e5; | |||
| } | |||
| .border-bottom { | |||
| border-bottom: 1px solid #e5e5e5; | |||
| } | |||
| .box-shadow { | |||
| box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); | |||
| } | |||
| button.accept-policy { | |||
| font-size: 1rem; | |||
| line-height: inherit; | |||
| } | |||
| @@ -0,0 +1,2 @@ | |||
| <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script> | |||
| <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> | |||
| @@ -0,0 +1,3 @@ | |||
| @using LLama.Web | |||
| @namespace LLama.Web.Pages | |||
| @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers | |||
| @@ -0,0 +1,3 @@ | |||
| @{ | |||
| Layout = "_Layout"; | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| using LLama.Web.Hubs; | |||
| using LLama.Web.Models; | |||
| using LLama.Web.Services; | |||
| namespace LLama.Web | |||
| { | |||
| public class Program | |||
| { | |||
| public static void Main(string[] args) | |||
| { | |||
| var builder = WebApplication.CreateBuilder(args); | |||
| // Add services to the container. | |||
| builder.Services.AddRazorPages(); | |||
| builder.Services.AddSignalR(); | |||
| // Load InteractiveOptions | |||
| builder.Services.AddOptions<LLamaOptions>() | |||
| .PostConfigure(x => x.Initialize()) | |||
| .BindConfiguration(nameof(LLamaOptions)); | |||
| // Services DI | |||
| builder.Services.AddSingleton<IModelSessionService, ModelSessionService>(); | |||
| var app = builder.Build(); | |||
| // Configure the HTTP request pipeline. | |||
| if (!app.Environment.IsDevelopment()) | |||
| { | |||
| app.UseExceptionHandler("/Error"); | |||
| // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. | |||
| app.UseHsts(); | |||
| } | |||
| app.UseHttpsRedirection(); | |||
| app.UseStaticFiles(); | |||
| app.UseRouting(); | |||
| app.UseAuthorization(); | |||
| app.MapRazorPages(); | |||
| app.MapHub<InteractiveHub>(nameof(InteractiveHub)); | |||
| app.Run(); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| namespace LLama.Web.Services | |||
| { | |||
| public interface IModelSessionService | |||
| { | |||
| } | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| namespace LLama.Web.Services | |||
| { | |||
| public class ModelSessionService : IModelSessionService | |||
| { | |||
| private readonly ILogger<ModelSessionService> _logger; | |||
| public ModelSessionService(ILogger<ModelSessionService> logger) | |||
| { | |||
| _logger = logger; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,9 @@ | |||
| { | |||
| "DetailedErrors": true, | |||
| "Logging": { | |||
| "LogLevel": { | |||
| "Default": "Information", | |||
| "Microsoft.AspNetCore": "Warning" | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,50 @@ | |||
| { | |||
| "Logging": { | |||
| "LogLevel": { | |||
| "Default": "Information", | |||
| "Microsoft.AspNetCore": "Warning" | |||
| } | |||
| }, | |||
| "AllowedHosts": "*", | |||
| "LLamaOptions": { | |||
| "Models": [ | |||
| { | |||
| "Name": "WizardLM-7B", | |||
| "ModelPath": "D:\\Repositories\\AI\\Models\\wizardLM-7B.ggmlv3.q4_0.bin", | |||
| "ContextSize": 2048, | |||
| "MaxInstances": 4 | |||
| } | |||
| ], | |||
| "Parameters": [ | |||
| { | |||
| "Name": "Default", | |||
| "Temperature": 0.6 | |||
| } | |||
| ], | |||
| "Prompts": [ | |||
| { | |||
| "Name": "Alpaca", | |||
| "Path": "D:\\Repositories\\AI\\Prompts\\alpaca.txt", | |||
| "AntiPrompt": [ | |||
| "User:" | |||
| ], | |||
| "OutputFilter": [ | |||
| "Response:", | |||
| "User:" | |||
| ] | |||
| }, | |||
| { | |||
| "Name": "ChatWithBob", | |||
| "Path": "D:\\Repositories\\AI\\Prompts\\chat-with-bob.txt", | |||
| "AntiPrompt": [ | |||
| "User:" | |||
| ], | |||
| "OutputFilter": [ | |||
| "Bob:", | |||
| "User:" | |||
| ] | |||
| } | |||
| ] | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| html, body { | |||
| font-size: 14px; | |||
| height: 100%; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| header { | |||
| } | |||
| main { | |||
| flex: 1; | |||
| overflow: auto; | |||
| } | |||
| footer { | |||
| bottom: 0; | |||
| width: 100%; | |||
| white-space: nowrap; | |||
| line-height: 60px; | |||
| } | |||
| @media (min-width: 768px) { | |||
| html { | |||
| font-size: 16px; | |||
| } | |||
| } | |||
| .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { | |||
| box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; | |||
| } | |||
| @@ -0,0 +1,4 @@ | |||
| // Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification | |||
| // for details on configuring this project to bundle and minify static web assets. | |||
| // Write your JavaScript code. | |||
| @@ -0,0 +1,22 @@ | |||
| The MIT License (MIT) | |||
| Copyright (c) 2011-2021 Twitter, Inc. | |||
| Copyright (c) 2011-2021 The Bootstrap Authors | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in | |||
| all copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
| THE SOFTWARE. | |||
| @@ -0,0 +1,427 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2021 The Bootstrap Authors | |||
| * Copyright 2011-2021 Twitter, Inc. | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | |||
| */ | |||
| *, | |||
| *::before, | |||
| *::after { | |||
| box-sizing: border-box; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| :root { | |||
| scroll-behavior: smooth; | |||
| } | |||
| } | |||
| body { | |||
| margin: 0; | |||
| font-family: var(--bs-body-font-family); | |||
| font-size: var(--bs-body-font-size); | |||
| font-weight: var(--bs-body-font-weight); | |||
| line-height: var(--bs-body-line-height); | |||
| color: var(--bs-body-color); | |||
| text-align: var(--bs-body-text-align); | |||
| background-color: var(--bs-body-bg); | |||
| -webkit-text-size-adjust: 100%; | |||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||
| } | |||
| hr { | |||
| margin: 1rem 0; | |||
| color: inherit; | |||
| background-color: currentColor; | |||
| border: 0; | |||
| opacity: 0.25; | |||
| } | |||
| hr:not([size]) { | |||
| height: 1px; | |||
| } | |||
| h6, h5, h4, h3, h2, h1 { | |||
| margin-top: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-weight: 500; | |||
| line-height: 1.2; | |||
| } | |||
| h1 { | |||
| font-size: calc(1.375rem + 1.5vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h1 { | |||
| font-size: 2.5rem; | |||
| } | |||
| } | |||
| h2 { | |||
| font-size: calc(1.325rem + 0.9vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h2 { | |||
| font-size: 2rem; | |||
| } | |||
| } | |||
| h3 { | |||
| font-size: calc(1.3rem + 0.6vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h3 { | |||
| font-size: 1.75rem; | |||
| } | |||
| } | |||
| h4 { | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h4 { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| h5 { | |||
| font-size: 1.25rem; | |||
| } | |||
| h6 { | |||
| font-size: 1rem; | |||
| } | |||
| p { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| abbr[title], | |||
| abbr[data-bs-original-title] { | |||
| -webkit-text-decoration: underline dotted; | |||
| text-decoration: underline dotted; | |||
| cursor: help; | |||
| -webkit-text-decoration-skip-ink: none; | |||
| text-decoration-skip-ink: none; | |||
| } | |||
| address { | |||
| margin-bottom: 1rem; | |||
| font-style: normal; | |||
| line-height: inherit; | |||
| } | |||
| ol, | |||
| ul { | |||
| padding-left: 2rem; | |||
| } | |||
| ol, | |||
| ul, | |||
| dl { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| ol ol, | |||
| ul ul, | |||
| ol ul, | |||
| ul ol { | |||
| margin-bottom: 0; | |||
| } | |||
| dt { | |||
| font-weight: 700; | |||
| } | |||
| dd { | |||
| margin-bottom: 0.5rem; | |||
| margin-left: 0; | |||
| } | |||
| blockquote { | |||
| margin: 0 0 1rem; | |||
| } | |||
| b, | |||
| strong { | |||
| font-weight: bolder; | |||
| } | |||
| small { | |||
| font-size: 0.875em; | |||
| } | |||
| mark { | |||
| padding: 0.2em; | |||
| background-color: #fcf8e3; | |||
| } | |||
| sub, | |||
| sup { | |||
| position: relative; | |||
| font-size: 0.75em; | |||
| line-height: 0; | |||
| vertical-align: baseline; | |||
| } | |||
| sub { | |||
| bottom: -0.25em; | |||
| } | |||
| sup { | |||
| top: -0.5em; | |||
| } | |||
| a { | |||
| color: #0d6efd; | |||
| text-decoration: underline; | |||
| } | |||
| a:hover { | |||
| color: #0a58ca; | |||
| } | |||
| a:not([href]):not([class]), a:not([href]):not([class]):hover { | |||
| color: inherit; | |||
| text-decoration: none; | |||
| } | |||
| pre, | |||
| code, | |||
| kbd, | |||
| samp { | |||
| font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |||
| font-size: 1em; | |||
| direction: ltr /* rtl:ignore */; | |||
| unicode-bidi: bidi-override; | |||
| } | |||
| pre { | |||
| display: block; | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| overflow: auto; | |||
| font-size: 0.875em; | |||
| } | |||
| pre code { | |||
| font-size: inherit; | |||
| color: inherit; | |||
| word-break: normal; | |||
| } | |||
| code { | |||
| font-size: 0.875em; | |||
| color: #d63384; | |||
| word-wrap: break-word; | |||
| } | |||
| a > code { | |||
| color: inherit; | |||
| } | |||
| kbd { | |||
| padding: 0.2rem 0.4rem; | |||
| font-size: 0.875em; | |||
| color: #fff; | |||
| background-color: #212529; | |||
| border-radius: 0.2rem; | |||
| } | |||
| kbd kbd { | |||
| padding: 0; | |||
| font-size: 1em; | |||
| font-weight: 700; | |||
| } | |||
| figure { | |||
| margin: 0 0 1rem; | |||
| } | |||
| img, | |||
| svg { | |||
| vertical-align: middle; | |||
| } | |||
| table { | |||
| caption-side: bottom; | |||
| border-collapse: collapse; | |||
| } | |||
| caption { | |||
| padding-top: 0.5rem; | |||
| padding-bottom: 0.5rem; | |||
| color: #6c757d; | |||
| text-align: left; | |||
| } | |||
| th { | |||
| text-align: inherit; | |||
| text-align: -webkit-match-parent; | |||
| } | |||
| thead, | |||
| tbody, | |||
| tfoot, | |||
| tr, | |||
| td, | |||
| th { | |||
| border-color: inherit; | |||
| border-style: solid; | |||
| border-width: 0; | |||
| } | |||
| label { | |||
| display: inline-block; | |||
| } | |||
| button { | |||
| border-radius: 0; | |||
| } | |||
| button:focus:not(:focus-visible) { | |||
| outline: 0; | |||
| } | |||
| input, | |||
| button, | |||
| select, | |||
| optgroup, | |||
| textarea { | |||
| margin: 0; | |||
| font-family: inherit; | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| } | |||
| button, | |||
| select { | |||
| text-transform: none; | |||
| } | |||
| [role=button] { | |||
| cursor: pointer; | |||
| } | |||
| select { | |||
| word-wrap: normal; | |||
| } | |||
| select:disabled { | |||
| opacity: 1; | |||
| } | |||
| [list]::-webkit-calendar-picker-indicator { | |||
| display: none; | |||
| } | |||
| button, | |||
| [type=button], | |||
| [type=reset], | |||
| [type=submit] { | |||
| -webkit-appearance: button; | |||
| } | |||
| button:not(:disabled), | |||
| [type=button]:not(:disabled), | |||
| [type=reset]:not(:disabled), | |||
| [type=submit]:not(:disabled) { | |||
| cursor: pointer; | |||
| } | |||
| ::-moz-focus-inner { | |||
| padding: 0; | |||
| border-style: none; | |||
| } | |||
| textarea { | |||
| resize: vertical; | |||
| } | |||
| fieldset { | |||
| min-width: 0; | |||
| padding: 0; | |||
| margin: 0; | |||
| border: 0; | |||
| } | |||
| legend { | |||
| float: left; | |||
| width: 100%; | |||
| padding: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| line-height: inherit; | |||
| } | |||
| @media (min-width: 1200px) { | |||
| legend { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| legend + * { | |||
| clear: left; | |||
| } | |||
| ::-webkit-datetime-edit-fields-wrapper, | |||
| ::-webkit-datetime-edit-text, | |||
| ::-webkit-datetime-edit-minute, | |||
| ::-webkit-datetime-edit-hour-field, | |||
| ::-webkit-datetime-edit-day-field, | |||
| ::-webkit-datetime-edit-month-field, | |||
| ::-webkit-datetime-edit-year-field { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-inner-spin-button { | |||
| height: auto; | |||
| } | |||
| [type=search] { | |||
| outline-offset: -2px; | |||
| -webkit-appearance: textfield; | |||
| } | |||
| /* rtl:raw: | |||
| [type="tel"], | |||
| [type="url"], | |||
| [type="email"], | |||
| [type="number"] { | |||
| direction: ltr; | |||
| } | |||
| */ | |||
| ::-webkit-search-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ::-webkit-color-swatch-wrapper { | |||
| padding: 0; | |||
| } | |||
| ::file-selector-button { | |||
| font: inherit; | |||
| } | |||
| ::-webkit-file-upload-button { | |||
| font: inherit; | |||
| -webkit-appearance: button; | |||
| } | |||
| output { | |||
| display: inline-block; | |||
| } | |||
| iframe { | |||
| border: 0; | |||
| } | |||
| summary { | |||
| display: list-item; | |||
| cursor: pointer; | |||
| } | |||
| progress { | |||
| vertical-align: baseline; | |||
| } | |||
| [hidden] { | |||
| display: none !important; | |||
| } | |||
| /*# sourceMappingURL=bootstrap-reboot.css.map */ | |||
| @@ -0,0 +1,8 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2021 The Bootstrap Authors | |||
| * Copyright 2011-2021 Twitter, Inc. | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | |||
| */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} | |||
| /*# sourceMappingURL=bootstrap-reboot.min.css.map */ | |||
| @@ -0,0 +1,424 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2021 The Bootstrap Authors | |||
| * Copyright 2011-2021 Twitter, Inc. | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | |||
| */ | |||
| *, | |||
| *::before, | |||
| *::after { | |||
| box-sizing: border-box; | |||
| } | |||
| @media (prefers-reduced-motion: no-preference) { | |||
| :root { | |||
| scroll-behavior: smooth; | |||
| } | |||
| } | |||
| body { | |||
| margin: 0; | |||
| font-family: var(--bs-body-font-family); | |||
| font-size: var(--bs-body-font-size); | |||
| font-weight: var(--bs-body-font-weight); | |||
| line-height: var(--bs-body-line-height); | |||
| color: var(--bs-body-color); | |||
| text-align: var(--bs-body-text-align); | |||
| background-color: var(--bs-body-bg); | |||
| -webkit-text-size-adjust: 100%; | |||
| -webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |||
| } | |||
| hr { | |||
| margin: 1rem 0; | |||
| color: inherit; | |||
| background-color: currentColor; | |||
| border: 0; | |||
| opacity: 0.25; | |||
| } | |||
| hr:not([size]) { | |||
| height: 1px; | |||
| } | |||
| h6, h5, h4, h3, h2, h1 { | |||
| margin-top: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-weight: 500; | |||
| line-height: 1.2; | |||
| } | |||
| h1 { | |||
| font-size: calc(1.375rem + 1.5vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h1 { | |||
| font-size: 2.5rem; | |||
| } | |||
| } | |||
| h2 { | |||
| font-size: calc(1.325rem + 0.9vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h2 { | |||
| font-size: 2rem; | |||
| } | |||
| } | |||
| h3 { | |||
| font-size: calc(1.3rem + 0.6vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h3 { | |||
| font-size: 1.75rem; | |||
| } | |||
| } | |||
| h4 { | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| } | |||
| @media (min-width: 1200px) { | |||
| h4 { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| h5 { | |||
| font-size: 1.25rem; | |||
| } | |||
| h6 { | |||
| font-size: 1rem; | |||
| } | |||
| p { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| abbr[title], | |||
| abbr[data-bs-original-title] { | |||
| -webkit-text-decoration: underline dotted; | |||
| text-decoration: underline dotted; | |||
| cursor: help; | |||
| -webkit-text-decoration-skip-ink: none; | |||
| text-decoration-skip-ink: none; | |||
| } | |||
| address { | |||
| margin-bottom: 1rem; | |||
| font-style: normal; | |||
| line-height: inherit; | |||
| } | |||
| ol, | |||
| ul { | |||
| padding-right: 2rem; | |||
| } | |||
| ol, | |||
| ul, | |||
| dl { | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| } | |||
| ol ol, | |||
| ul ul, | |||
| ol ul, | |||
| ul ol { | |||
| margin-bottom: 0; | |||
| } | |||
| dt { | |||
| font-weight: 700; | |||
| } | |||
| dd { | |||
| margin-bottom: 0.5rem; | |||
| margin-right: 0; | |||
| } | |||
| blockquote { | |||
| margin: 0 0 1rem; | |||
| } | |||
| b, | |||
| strong { | |||
| font-weight: bolder; | |||
| } | |||
| small { | |||
| font-size: 0.875em; | |||
| } | |||
| mark { | |||
| padding: 0.2em; | |||
| background-color: #fcf8e3; | |||
| } | |||
| sub, | |||
| sup { | |||
| position: relative; | |||
| font-size: 0.75em; | |||
| line-height: 0; | |||
| vertical-align: baseline; | |||
| } | |||
| sub { | |||
| bottom: -0.25em; | |||
| } | |||
| sup { | |||
| top: -0.5em; | |||
| } | |||
| a { | |||
| color: #0d6efd; | |||
| text-decoration: underline; | |||
| } | |||
| a:hover { | |||
| color: #0a58ca; | |||
| } | |||
| a:not([href]):not([class]), a:not([href]):not([class]):hover { | |||
| color: inherit; | |||
| text-decoration: none; | |||
| } | |||
| pre, | |||
| code, | |||
| kbd, | |||
| samp { | |||
| font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |||
| font-size: 1em; | |||
| direction: ltr ; | |||
| unicode-bidi: bidi-override; | |||
| } | |||
| pre { | |||
| display: block; | |||
| margin-top: 0; | |||
| margin-bottom: 1rem; | |||
| overflow: auto; | |||
| font-size: 0.875em; | |||
| } | |||
| pre code { | |||
| font-size: inherit; | |||
| color: inherit; | |||
| word-break: normal; | |||
| } | |||
| code { | |||
| font-size: 0.875em; | |||
| color: #d63384; | |||
| word-wrap: break-word; | |||
| } | |||
| a > code { | |||
| color: inherit; | |||
| } | |||
| kbd { | |||
| padding: 0.2rem 0.4rem; | |||
| font-size: 0.875em; | |||
| color: #fff; | |||
| background-color: #212529; | |||
| border-radius: 0.2rem; | |||
| } | |||
| kbd kbd { | |||
| padding: 0; | |||
| font-size: 1em; | |||
| font-weight: 700; | |||
| } | |||
| figure { | |||
| margin: 0 0 1rem; | |||
| } | |||
| img, | |||
| svg { | |||
| vertical-align: middle; | |||
| } | |||
| table { | |||
| caption-side: bottom; | |||
| border-collapse: collapse; | |||
| } | |||
| caption { | |||
| padding-top: 0.5rem; | |||
| padding-bottom: 0.5rem; | |||
| color: #6c757d; | |||
| text-align: right; | |||
| } | |||
| th { | |||
| text-align: inherit; | |||
| text-align: -webkit-match-parent; | |||
| } | |||
| thead, | |||
| tbody, | |||
| tfoot, | |||
| tr, | |||
| td, | |||
| th { | |||
| border-color: inherit; | |||
| border-style: solid; | |||
| border-width: 0; | |||
| } | |||
| label { | |||
| display: inline-block; | |||
| } | |||
| button { | |||
| border-radius: 0; | |||
| } | |||
| button:focus:not(:focus-visible) { | |||
| outline: 0; | |||
| } | |||
| input, | |||
| button, | |||
| select, | |||
| optgroup, | |||
| textarea { | |||
| margin: 0; | |||
| font-family: inherit; | |||
| font-size: inherit; | |||
| line-height: inherit; | |||
| } | |||
| button, | |||
| select { | |||
| text-transform: none; | |||
| } | |||
| [role=button] { | |||
| cursor: pointer; | |||
| } | |||
| select { | |||
| word-wrap: normal; | |||
| } | |||
| select:disabled { | |||
| opacity: 1; | |||
| } | |||
| [list]::-webkit-calendar-picker-indicator { | |||
| display: none; | |||
| } | |||
| button, | |||
| [type=button], | |||
| [type=reset], | |||
| [type=submit] { | |||
| -webkit-appearance: button; | |||
| } | |||
| button:not(:disabled), | |||
| [type=button]:not(:disabled), | |||
| [type=reset]:not(:disabled), | |||
| [type=submit]:not(:disabled) { | |||
| cursor: pointer; | |||
| } | |||
| ::-moz-focus-inner { | |||
| padding: 0; | |||
| border-style: none; | |||
| } | |||
| textarea { | |||
| resize: vertical; | |||
| } | |||
| fieldset { | |||
| min-width: 0; | |||
| padding: 0; | |||
| margin: 0; | |||
| border: 0; | |||
| } | |||
| legend { | |||
| float: right; | |||
| width: 100%; | |||
| padding: 0; | |||
| margin-bottom: 0.5rem; | |||
| font-size: calc(1.275rem + 0.3vw); | |||
| line-height: inherit; | |||
| } | |||
| @media (min-width: 1200px) { | |||
| legend { | |||
| font-size: 1.5rem; | |||
| } | |||
| } | |||
| legend + * { | |||
| clear: right; | |||
| } | |||
| ::-webkit-datetime-edit-fields-wrapper, | |||
| ::-webkit-datetime-edit-text, | |||
| ::-webkit-datetime-edit-minute, | |||
| ::-webkit-datetime-edit-hour-field, | |||
| ::-webkit-datetime-edit-day-field, | |||
| ::-webkit-datetime-edit-month-field, | |||
| ::-webkit-datetime-edit-year-field { | |||
| padding: 0; | |||
| } | |||
| ::-webkit-inner-spin-button { | |||
| height: auto; | |||
| } | |||
| [type=search] { | |||
| outline-offset: -2px; | |||
| -webkit-appearance: textfield; | |||
| } | |||
| [type="tel"], | |||
| [type="url"], | |||
| [type="email"], | |||
| [type="number"] { | |||
| direction: ltr; | |||
| } | |||
| ::-webkit-search-decoration { | |||
| -webkit-appearance: none; | |||
| } | |||
| ::-webkit-color-swatch-wrapper { | |||
| padding: 0; | |||
| } | |||
| ::file-selector-button { | |||
| font: inherit; | |||
| } | |||
| ::-webkit-file-upload-button { | |||
| font: inherit; | |||
| -webkit-appearance: button; | |||
| } | |||
| output { | |||
| display: inline-block; | |||
| } | |||
| iframe { | |||
| border: 0; | |||
| } | |||
| summary { | |||
| display: list-item; | |||
| cursor: pointer; | |||
| } | |||
| progress { | |||
| vertical-align: baseline; | |||
| } | |||
| [hidden] { | |||
| display: none !important; | |||
| } | |||
| /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ | |||
| @@ -0,0 +1,8 @@ | |||
| /*! | |||
| * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/) | |||
| * Copyright 2011-2021 The Bootstrap Authors | |||
| * Copyright 2011-2021 Twitter, Inc. | |||
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) | |||
| * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) | |||
| */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} | |||
| /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ | |||
| @@ -0,0 +1,23 @@ | |||
| The MIT License (MIT) | |||
| Copyright (c) .NET Foundation and Contributors | |||
| All rights reserved. | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in all | |||
| copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| SOFTWARE. | |||
| @@ -0,0 +1,435 @@ | |||
| /** | |||
| * @license | |||
| * Unobtrusive validation support library for jQuery and jQuery Validate | |||
| * Copyright (c) .NET Foundation. All rights reserved. | |||
| * Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | |||
| * @version v4.0.0 | |||
| */ | |||
| /*jslint white: true, browser: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: false */ | |||
| /*global document: false, jQuery: false */ | |||
| (function (factory) { | |||
| if (typeof define === 'function' && define.amd) { | |||
| // AMD. Register as an anonymous module. | |||
| define("jquery.validate.unobtrusive", ['jquery-validation'], factory); | |||
| } else if (typeof module === 'object' && module.exports) { | |||
| // CommonJS-like environments that support module.exports | |||
| module.exports = factory(require('jquery-validation')); | |||
| } else { | |||
| // Browser global | |||
| jQuery.validator.unobtrusive = factory(jQuery); | |||
| } | |||
| }(function ($) { | |||
| var $jQval = $.validator, | |||
| adapters, | |||
| data_validation = "unobtrusiveValidation"; | |||
| function setValidationValues(options, ruleName, value) { | |||
| options.rules[ruleName] = value; | |||
| if (options.message) { | |||
| options.messages[ruleName] = options.message; | |||
| } | |||
| } | |||
| function splitAndTrim(value) { | |||
| return value.replace(/^\s+|\s+$/g, "").split(/\s*,\s*/g); | |||
| } | |||
| function escapeAttributeValue(value) { | |||
| // As mentioned on http://api.jquery.com/category/selectors/ | |||
| return value.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g, "\\$1"); | |||
| } | |||
| function getModelPrefix(fieldName) { | |||
| return fieldName.substr(0, fieldName.lastIndexOf(".") + 1); | |||
| } | |||
| function appendModelPrefix(value, prefix) { | |||
| if (value.indexOf("*.") === 0) { | |||
| value = value.replace("*.", prefix); | |||
| } | |||
| return value; | |||
| } | |||
| function onError(error, inputElement) { // 'this' is the form element | |||
| var container = $(this).find("[data-valmsg-for='" + escapeAttributeValue(inputElement[0].name) + "']"), | |||
| replaceAttrValue = container.attr("data-valmsg-replace"), | |||
| replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) !== false : null; | |||
| container.removeClass("field-validation-valid").addClass("field-validation-error"); | |||
| error.data("unobtrusiveContainer", container); | |||
| if (replace) { | |||
| container.empty(); | |||
| error.removeClass("input-validation-error").appendTo(container); | |||
| } | |||
| else { | |||
| error.hide(); | |||
| } | |||
| } | |||
| function onErrors(event, validator) { // 'this' is the form element | |||
| var container = $(this).find("[data-valmsg-summary=true]"), | |||
| list = container.find("ul"); | |||
| if (list && list.length && validator.errorList.length) { | |||
| list.empty(); | |||
| container.addClass("validation-summary-errors").removeClass("validation-summary-valid"); | |||
| $.each(validator.errorList, function () { | |||
| $("<li />").html(this.message).appendTo(list); | |||
| }); | |||
| } | |||
| } | |||
| function onSuccess(error) { // 'this' is the form element | |||
| var container = error.data("unobtrusiveContainer"); | |||
| if (container) { | |||
| var replaceAttrValue = container.attr("data-valmsg-replace"), | |||
| replace = replaceAttrValue ? $.parseJSON(replaceAttrValue) : null; | |||
| container.addClass("field-validation-valid").removeClass("field-validation-error"); | |||
| error.removeData("unobtrusiveContainer"); | |||
| if (replace) { | |||
| container.empty(); | |||
| } | |||
| } | |||
| } | |||
| function onReset(event) { // 'this' is the form element | |||
| var $form = $(this), | |||
| key = '__jquery_unobtrusive_validation_form_reset'; | |||
| if ($form.data(key)) { | |||
| return; | |||
| } | |||
| // Set a flag that indicates we're currently resetting the form. | |||
| $form.data(key, true); | |||
| try { | |||
| $form.data("validator").resetForm(); | |||
| } finally { | |||
| $form.removeData(key); | |||
| } | |||
| $form.find(".validation-summary-errors") | |||
| .addClass("validation-summary-valid") | |||
| .removeClass("validation-summary-errors"); | |||
| $form.find(".field-validation-error") | |||
| .addClass("field-validation-valid") | |||
| .removeClass("field-validation-error") | |||
| .removeData("unobtrusiveContainer") | |||
| .find(">*") // If we were using valmsg-replace, get the underlying error | |||
| .removeData("unobtrusiveContainer"); | |||
| } | |||
| function validationInfo(form) { | |||
| var $form = $(form), | |||
| result = $form.data(data_validation), | |||
| onResetProxy = $.proxy(onReset, form), | |||
| defaultOptions = $jQval.unobtrusive.options || {}, | |||
| execInContext = function (name, args) { | |||
| var func = defaultOptions[name]; | |||
| func && $.isFunction(func) && func.apply(form, args); | |||
| }; | |||
| if (!result) { | |||
| result = { | |||
| options: { // options structure passed to jQuery Validate's validate() method | |||
| errorClass: defaultOptions.errorClass || "input-validation-error", | |||
| errorElement: defaultOptions.errorElement || "span", | |||
| errorPlacement: function () { | |||
| onError.apply(form, arguments); | |||
| execInContext("errorPlacement", arguments); | |||
| }, | |||
| invalidHandler: function () { | |||
| onErrors.apply(form, arguments); | |||
| execInContext("invalidHandler", arguments); | |||
| }, | |||
| messages: {}, | |||
| rules: {}, | |||
| success: function () { | |||
| onSuccess.apply(form, arguments); | |||
| execInContext("success", arguments); | |||
| } | |||
| }, | |||
| attachValidation: function () { | |||
| $form | |||
| .off("reset." + data_validation, onResetProxy) | |||
| .on("reset." + data_validation, onResetProxy) | |||
| .validate(this.options); | |||
| }, | |||
| validate: function () { // a validation function that is called by unobtrusive Ajax | |||
| $form.validate(); | |||
| return $form.valid(); | |||
| } | |||
| }; | |||
| $form.data(data_validation, result); | |||
| } | |||
| return result; | |||
| } | |||
| $jQval.unobtrusive = { | |||
| adapters: [], | |||
| parseElement: function (element, skipAttach) { | |||
| /// <summary> | |||
| /// Parses a single HTML element for unobtrusive validation attributes. | |||
| /// </summary> | |||
| /// <param name="element" domElement="true">The HTML element to be parsed.</param> | |||
| /// <param name="skipAttach" type="Boolean">[Optional] true to skip attaching the | |||
| /// validation to the form. If parsing just this single element, you should specify true. | |||
| /// If parsing several elements, you should specify false, and manually attach the validation | |||
| /// to the form when you are finished. The default is false.</param> | |||
| var $element = $(element), | |||
| form = $element.parents("form")[0], | |||
| valInfo, rules, messages; | |||
| if (!form) { // Cannot do client-side validation without a form | |||
| return; | |||
| } | |||
| valInfo = validationInfo(form); | |||
| valInfo.options.rules[element.name] = rules = {}; | |||
| valInfo.options.messages[element.name] = messages = {}; | |||
| $.each(this.adapters, function () { | |||
| var prefix = "data-val-" + this.name, | |||
| message = $element.attr(prefix), | |||
| paramValues = {}; | |||
| if (message !== undefined) { // Compare against undefined, because an empty message is legal (and falsy) | |||
| prefix += "-"; | |||
| $.each(this.params, function () { | |||
| paramValues[this] = $element.attr(prefix + this); | |||
| }); | |||
| this.adapt({ | |||
| element: element, | |||
| form: form, | |||
| message: message, | |||
| params: paramValues, | |||
| rules: rules, | |||
| messages: messages | |||
| }); | |||
| } | |||
| }); | |||
| $.extend(rules, { "__dummy__": true }); | |||
| if (!skipAttach) { | |||
| valInfo.attachValidation(); | |||
| } | |||
| }, | |||
| parse: function (selector) { | |||
| /// <summary> | |||
| /// Parses all the HTML elements in the specified selector. It looks for input elements decorated | |||
| /// with the [data-val=true] attribute value and enables validation according to the data-val-* | |||
| /// attribute values. | |||
| /// </summary> | |||
| /// <param name="selector" type="String">Any valid jQuery selector.</param> | |||
| // $forms includes all forms in selector's DOM hierarchy (parent, children and self) that have at least one | |||
| // element with data-val=true | |||
| var $selector = $(selector), | |||
| $forms = $selector.parents() | |||
| .addBack() | |||
| .filter("form") | |||
| .add($selector.find("form")) | |||
| .has("[data-val=true]"); | |||
| $selector.find("[data-val=true]").each(function () { | |||
| $jQval.unobtrusive.parseElement(this, true); | |||
| }); | |||
| $forms.each(function () { | |||
| var info = validationInfo(this); | |||
| if (info) { | |||
| info.attachValidation(); | |||
| } | |||
| }); | |||
| } | |||
| }; | |||
| adapters = $jQval.unobtrusive.adapters; | |||
| adapters.add = function (adapterName, params, fn) { | |||
| /// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation.</summary> | |||
| /// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used | |||
| /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param> | |||
| /// <param name="params" type="Array" optional="true">[Optional] An array of parameter names (strings) that will | |||
| /// be extracted from the data-val-nnnn-mmmm HTML attributes (where nnnn is the adapter name, and | |||
| /// mmmm is the parameter name).</param> | |||
| /// <param name="fn" type="Function">The function to call, which adapts the values from the HTML | |||
| /// attributes into jQuery Validate rules and/or messages.</param> | |||
| /// <returns type="jQuery.validator.unobtrusive.adapters" /> | |||
| if (!fn) { // Called with no params, just a function | |||
| fn = params; | |||
| params = []; | |||
| } | |||
| this.push({ name: adapterName, params: params, adapt: fn }); | |||
| return this; | |||
| }; | |||
| adapters.addBool = function (adapterName, ruleName) { | |||
| /// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where | |||
| /// the jQuery Validate validation rule has no parameter values.</summary> | |||
| /// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used | |||
| /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param> | |||
| /// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value | |||
| /// of adapterName will be used instead.</param> | |||
| /// <returns type="jQuery.validator.unobtrusive.adapters" /> | |||
| return this.add(adapterName, function (options) { | |||
| setValidationValues(options, ruleName || adapterName, true); | |||
| }); | |||
| }; | |||
| adapters.addMinMax = function (adapterName, minRuleName, maxRuleName, minMaxRuleName, minAttribute, maxAttribute) { | |||
| /// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where | |||
| /// the jQuery Validate validation has three potential rules (one for min-only, one for max-only, and | |||
| /// one for min-and-max). The HTML parameters are expected to be named -min and -max.</summary> | |||
| /// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used | |||
| /// in the data-val-nnnn HTML attribute (where nnnn is the adapter name).</param> | |||
| /// <param name="minRuleName" type="String">The name of the jQuery Validate rule to be used when you only | |||
| /// have a minimum value.</param> | |||
| /// <param name="maxRuleName" type="String">The name of the jQuery Validate rule to be used when you only | |||
| /// have a maximum value.</param> | |||
| /// <param name="minMaxRuleName" type="String">The name of the jQuery Validate rule to be used when you | |||
| /// have both a minimum and maximum value.</param> | |||
| /// <param name="minAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that | |||
| /// contains the minimum value. The default is "min".</param> | |||
| /// <param name="maxAttribute" type="String" optional="true">[Optional] The name of the HTML attribute that | |||
| /// contains the maximum value. The default is "max".</param> | |||
| /// <returns type="jQuery.validator.unobtrusive.adapters" /> | |||
| return this.add(adapterName, [minAttribute || "min", maxAttribute || "max"], function (options) { | |||
| var min = options.params.min, | |||
| max = options.params.max; | |||
| if (min && max) { | |||
| setValidationValues(options, minMaxRuleName, [min, max]); | |||
| } | |||
| else if (min) { | |||
| setValidationValues(options, minRuleName, min); | |||
| } | |||
| else if (max) { | |||
| setValidationValues(options, maxRuleName, max); | |||
| } | |||
| }); | |||
| }; | |||
| adapters.addSingleVal = function (adapterName, attribute, ruleName) { | |||
| /// <summary>Adds a new adapter to convert unobtrusive HTML into a jQuery Validate validation, where | |||
| /// the jQuery Validate validation rule has a single value.</summary> | |||
| /// <param name="adapterName" type="String">The name of the adapter to be added. This matches the name used | |||
| /// in the data-val-nnnn HTML attribute(where nnnn is the adapter name).</param> | |||
| /// <param name="attribute" type="String">[Optional] The name of the HTML attribute that contains the value. | |||
| /// The default is "val".</param> | |||
| /// <param name="ruleName" type="String" optional="true">[Optional] The name of the jQuery Validate rule. If not provided, the value | |||
| /// of adapterName will be used instead.</param> | |||
| /// <returns type="jQuery.validator.unobtrusive.adapters" /> | |||
| return this.add(adapterName, [attribute || "val"], function (options) { | |||
| setValidationValues(options, ruleName || adapterName, options.params[attribute]); | |||
| }); | |||
| }; | |||
| $jQval.addMethod("__dummy__", function (value, element, params) { | |||
| return true; | |||
| }); | |||
| $jQval.addMethod("regex", function (value, element, params) { | |||
| var match; | |||
| if (this.optional(element)) { | |||
| return true; | |||
| } | |||
| match = new RegExp(params).exec(value); | |||
| return (match && (match.index === 0) && (match[0].length === value.length)); | |||
| }); | |||
| $jQval.addMethod("nonalphamin", function (value, element, nonalphamin) { | |||
| var match; | |||
| if (nonalphamin) { | |||
| match = value.match(/\W/g); | |||
| match = match && match.length >= nonalphamin; | |||
| } | |||
| return match; | |||
| }); | |||
| if ($jQval.methods.extension) { | |||
| adapters.addSingleVal("accept", "mimtype"); | |||
| adapters.addSingleVal("extension", "extension"); | |||
| } else { | |||
| // for backward compatibility, when the 'extension' validation method does not exist, such as with versions | |||
| // of JQuery Validation plugin prior to 1.10, we should use the 'accept' method for | |||
| // validating the extension, and ignore mime-type validations as they are not supported. | |||
| adapters.addSingleVal("extension", "extension", "accept"); | |||
| } | |||
| adapters.addSingleVal("regex", "pattern"); | |||
| adapters.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"); | |||
| adapters.addMinMax("length", "minlength", "maxlength", "rangelength").addMinMax("range", "min", "max", "range"); | |||
| adapters.addMinMax("minlength", "minlength").addMinMax("maxlength", "minlength", "maxlength"); | |||
| adapters.add("equalto", ["other"], function (options) { | |||
| var prefix = getModelPrefix(options.element.name), | |||
| other = options.params.other, | |||
| fullOtherName = appendModelPrefix(other, prefix), | |||
| element = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(fullOtherName) + "']")[0]; | |||
| setValidationValues(options, "equalTo", element); | |||
| }); | |||
| adapters.add("required", function (options) { | |||
| // jQuery Validate equates "required" with "mandatory" for checkbox elements | |||
| if (options.element.tagName.toUpperCase() !== "INPUT" || options.element.type.toUpperCase() !== "CHECKBOX") { | |||
| setValidationValues(options, "required", true); | |||
| } | |||
| }); | |||
| adapters.add("remote", ["url", "type", "additionalfields"], function (options) { | |||
| var value = { | |||
| url: options.params.url, | |||
| type: options.params.type || "GET", | |||
| data: {} | |||
| }, | |||
| prefix = getModelPrefix(options.element.name); | |||
| $.each(splitAndTrim(options.params.additionalfields || options.element.name), function (i, fieldName) { | |||
| var paramName = appendModelPrefix(fieldName, prefix); | |||
| value.data[paramName] = function () { | |||
| var field = $(options.form).find(":input").filter("[name='" + escapeAttributeValue(paramName) + "']"); | |||
| // For checkboxes and radio buttons, only pick up values from checked fields. | |||
| if (field.is(":checkbox")) { | |||
| return field.filter(":checked").val() || field.filter(":hidden").val() || ''; | |||
| } | |||
| else if (field.is(":radio")) { | |||
| return field.filter(":checked").val() || ''; | |||
| } | |||
| return field.val(); | |||
| }; | |||
| }); | |||
| setValidationValues(options, "remote", value); | |||
| }); | |||
| adapters.add("password", ["min", "nonalphamin", "regex"], function (options) { | |||
| if (options.params.min) { | |||
| setValidationValues(options, "minlength", options.params.min); | |||
| } | |||
| if (options.params.nonalphamin) { | |||
| setValidationValues(options, "nonalphamin", options.params.nonalphamin); | |||
| } | |||
| if (options.params.regex) { | |||
| setValidationValues(options, "regex", options.params.regex); | |||
| } | |||
| }); | |||
| adapters.add("fileextensions", ["extensions"], function (options) { | |||
| setValidationValues(options, "extension", options.params.extensions); | |||
| }); | |||
| $(function () { | |||
| $jQval.unobtrusive.parse(document); | |||
| }); | |||
| return $jQval.unobtrusive; | |||
| })); | |||
| @@ -0,0 +1,22 @@ | |||
| The MIT License (MIT) | |||
| ===================== | |||
| Copyright Jörn Zaefferer | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in | |||
| all copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
| THE SOFTWARE. | |||
| @@ -0,0 +1,21 @@ | |||
| Copyright OpenJS Foundation and other contributors, https://openjsf.org/ | |||
| Permission is hereby granted, free of charge, to any person obtaining | |||
| a copy of this software and associated documentation files (the | |||
| "Software"), to deal in the Software without restriction, including | |||
| without limitation the rights to use, copy, modify, merge, publish, | |||
| distribute, sublicense, and/or sell copies of the Software, and to | |||
| permit persons to whom the Software is furnished to do so, subject to | |||
| the following conditions: | |||
| The above copyright notice and this permission notice shall be | |||
| included in all copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |||
| NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |||
| LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |||
| OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |||
| WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |||
| @@ -0,0 +1,632 @@ | |||
| /*! | |||
| * mustache.js - Logic-less {{mustache}} templates with JavaScript | |||
| * http://github.com/janl/mustache.js | |||
| */ | |||
| /*global define: false Mustache: true*/ | |||
| (function defineMustache(global, factory) { | |||
| if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { | |||
| factory(exports); // CommonJS | |||
| } else if (typeof define === 'function' && define.amd) { | |||
| define(['exports'], factory); // AMD | |||
| } else { | |||
| global.Mustache = {}; | |||
| factory(global.Mustache); // script, wsh, asp | |||
| } | |||
| }(this, function mustacheFactory(mustache) { | |||
| var objectToString = Object.prototype.toString; | |||
| var isArray = Array.isArray || function isArrayPolyfill(object) { | |||
| return objectToString.call(object) === '[object Array]'; | |||
| }; | |||
| function isFunction(object) { | |||
| return typeof object === 'function'; | |||
| } | |||
| /** | |||
| * More correct typeof string handling array | |||
| * which normally returns typeof 'object' | |||
| */ | |||
| function typeStr(obj) { | |||
| return isArray(obj) ? 'array' : typeof obj; | |||
| } | |||
| function escapeRegExp(string) { | |||
| return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); | |||
| } | |||
| /** | |||
| * Null safe way of checking whether or not an object, | |||
| * including its prototype, has a given property | |||
| */ | |||
| function hasProperty(obj, propName) { | |||
| return obj != null && typeof obj === 'object' && (propName in obj); | |||
| } | |||
| // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 | |||
| // See https://github.com/janl/mustache.js/issues/189 | |||
| var regExpTest = RegExp.prototype.test; | |||
| function testRegExp(re, string) { | |||
| return regExpTest.call(re, string); | |||
| } | |||
| var nonSpaceRe = /\S/; | |||
| function isWhitespace(string) { | |||
| return !testRegExp(nonSpaceRe, string); | |||
| } | |||
| var entityMap = { | |||
| '&': '&', | |||
| '<': '<', | |||
| '>': '>', | |||
| '"': '"', | |||
| "'": ''', | |||
| '/': '/', | |||
| '`': '`', | |||
| '=': '=' | |||
| }; | |||
| function escapeHtml(string) { | |||
| return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) { | |||
| return entityMap[s]; | |||
| }); | |||
| } | |||
| var whiteRe = /\s*/; | |||
| var spaceRe = /\s+/; | |||
| var equalsRe = /\s*=/; | |||
| var curlyRe = /\s*\}/; | |||
| var tagRe = /#|\^|\/|>|\{|&|=|!/; | |||
| /** | |||
| * Breaks up the given `template` string into a tree of tokens. If the `tags` | |||
| * argument is given here it must be an array with two string values: the | |||
| * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of | |||
| * course, the default is to use mustaches (i.e. mustache.tags). | |||
| * | |||
| * A token is an array with at least 4 elements. The first element is the | |||
| * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag | |||
| * did not contain a symbol (i.e. {{myValue}}) this element is "name". For | |||
| * all text that appears outside a symbol this element is "text". | |||
| * | |||
| * The second element of a token is its "value". For mustache tags this is | |||
| * whatever else was inside the tag besides the opening symbol. For text tokens | |||
| * this is the text itself. | |||
| * | |||
| * The third and fourth elements of the token are the start and end indices, | |||
| * respectively, of the token in the original template. | |||
| * | |||
| * Tokens that are the root node of a subtree contain two more elements: 1) an | |||
| * array of tokens in the subtree and 2) the index in the original template at | |||
| * which the closing tag for that section begins. | |||
| */ | |||
| function parseTemplate(template, tags) { | |||
| if (!template) | |||
| return []; | |||
| var sections = []; // Stack to hold section tokens | |||
| var tokens = []; // Buffer to hold the tokens | |||
| var spaces = []; // Indices of whitespace tokens on the current line | |||
| var hasTag = false; // Is there a {{tag}} on the current line? | |||
| var nonSpace = false; // Is there a non-space char on the current line? | |||
| // Strips all whitespace tokens array for the current line | |||
| // if there was a {{#tag}} on it and otherwise only space. | |||
| function stripSpace() { | |||
| if (hasTag && !nonSpace) { | |||
| while (spaces.length) | |||
| delete tokens[spaces.pop()]; | |||
| } else { | |||
| spaces = []; | |||
| } | |||
| hasTag = false; | |||
| nonSpace = false; | |||
| } | |||
| var openingTagRe, closingTagRe, closingCurlyRe; | |||
| function compileTags(tagsToCompile) { | |||
| if (typeof tagsToCompile === 'string') | |||
| tagsToCompile = tagsToCompile.split(spaceRe, 2); | |||
| if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) | |||
| throw new Error('Invalid tags: ' + tagsToCompile); | |||
| openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); | |||
| closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); | |||
| closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); | |||
| } | |||
| compileTags(tags || mustache.tags); | |||
| var scanner = new Scanner(template); | |||
| var start, type, value, chr, token, openSection; | |||
| while (!scanner.eos()) { | |||
| start = scanner.pos; | |||
| // Match any text between tags. | |||
| value = scanner.scanUntil(openingTagRe); | |||
| if (value) { | |||
| for (var i = 0, valueLength = value.length; i < valueLength; ++i) { | |||
| chr = value.charAt(i); | |||
| if (isWhitespace(chr)) { | |||
| spaces.push(tokens.length); | |||
| } else { | |||
| nonSpace = true; | |||
| } | |||
| tokens.push(['text', chr, start, start + 1]); | |||
| start += 1; | |||
| // Check for whitespace on the current line. | |||
| if (chr === '\n') | |||
| stripSpace(); | |||
| } | |||
| } | |||
| // Match the opening tag. | |||
| if (!scanner.scan(openingTagRe)) | |||
| break; | |||
| hasTag = true; | |||
| // Get the tag type. | |||
| type = scanner.scan(tagRe) || 'name'; | |||
| scanner.scan(whiteRe); | |||
| // Get the tag value. | |||
| if (type === '=') { | |||
| value = scanner.scanUntil(equalsRe); | |||
| scanner.scan(equalsRe); | |||
| scanner.scanUntil(closingTagRe); | |||
| } else if (type === '{') { | |||
| value = scanner.scanUntil(closingCurlyRe); | |||
| scanner.scan(curlyRe); | |||
| scanner.scanUntil(closingTagRe); | |||
| type = '&'; | |||
| } else { | |||
| value = scanner.scanUntil(closingTagRe); | |||
| } | |||
| // Match the closing tag. | |||
| if (!scanner.scan(closingTagRe)) | |||
| throw new Error('Unclosed tag at ' + scanner.pos); | |||
| token = [type, value, start, scanner.pos]; | |||
| tokens.push(token); | |||
| if (type === '#' || type === '^') { | |||
| sections.push(token); | |||
| } else if (type === '/') { | |||
| // Check section nesting. | |||
| openSection = sections.pop(); | |||
| if (!openSection) | |||
| throw new Error('Unopened section "' + value + '" at ' + start); | |||
| if (openSection[1] !== value) | |||
| throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); | |||
| } else if (type === 'name' || type === '{' || type === '&') { | |||
| nonSpace = true; | |||
| } else if (type === '=') { | |||
| // Set the tags for the next time around. | |||
| compileTags(value); | |||
| } | |||
| } | |||
| // Make sure there are no open sections when we're done. | |||
| openSection = sections.pop(); | |||
| if (openSection) | |||
| throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); | |||
| return nestTokens(squashTokens(tokens)); | |||
| } | |||
| /** | |||
| * Combines the values of consecutive text tokens in the given `tokens` array | |||
| * to a single token. | |||
| */ | |||
| function squashTokens(tokens) { | |||
| var squashedTokens = []; | |||
| var token, lastToken; | |||
| for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { | |||
| token = tokens[i]; | |||
| if (token) { | |||
| if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { | |||
| lastToken[1] += token[1]; | |||
| lastToken[3] = token[3]; | |||
| } else { | |||
| squashedTokens.push(token); | |||
| lastToken = token; | |||
| } | |||
| } | |||
| } | |||
| return squashedTokens; | |||
| } | |||
| /** | |||
| * Forms the given array of `tokens` into a nested tree structure where | |||
| * tokens that represent a section have two additional items: 1) an array of | |||
| * all tokens that appear in that section and 2) the index in the original | |||
| * template that represents the end of that section. | |||
| */ | |||
| function nestTokens(tokens) { | |||
| var nestedTokens = []; | |||
| var collector = nestedTokens; | |||
| var sections = []; | |||
| var token, section; | |||
| for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { | |||
| token = tokens[i]; | |||
| switch (token[0]) { | |||
| case '#': | |||
| case '^': | |||
| collector.push(token); | |||
| sections.push(token); | |||
| collector = token[4] = []; | |||
| break; | |||
| case '/': | |||
| section = sections.pop(); | |||
| section[5] = token[2]; | |||
| collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; | |||
| break; | |||
| default: | |||
| collector.push(token); | |||
| } | |||
| } | |||
| return nestedTokens; | |||
| } | |||
| /** | |||
| * A simple string scanner that is used by the template parser to find | |||
| * tokens in template strings. | |||
| */ | |||
| function Scanner(string) { | |||
| this.string = string; | |||
| this.tail = string; | |||
| this.pos = 0; | |||
| } | |||
| /** | |||
| * Returns `true` if the tail is empty (end of string). | |||
| */ | |||
| Scanner.prototype.eos = function eos() { | |||
| return this.tail === ''; | |||
| }; | |||
| /** | |||
| * Tries to match the given regular expression at the current position. | |||
| * Returns the matched text if it can match, the empty string otherwise. | |||
| */ | |||
| Scanner.prototype.scan = function scan(re) { | |||
| var match = this.tail.match(re); | |||
| if (!match || match.index !== 0) | |||
| return ''; | |||
| var string = match[0]; | |||
| this.tail = this.tail.substring(string.length); | |||
| this.pos += string.length; | |||
| return string; | |||
| }; | |||
| /** | |||
| * Skips all text until the given regular expression can be matched. Returns | |||
| * the skipped string, which is the entire tail if no match can be made. | |||
| */ | |||
| Scanner.prototype.scanUntil = function scanUntil(re) { | |||
| var index = this.tail.search(re), match; | |||
| switch (index) { | |||
| case -1: | |||
| match = this.tail; | |||
| this.tail = ''; | |||
| break; | |||
| case 0: | |||
| match = ''; | |||
| break; | |||
| default: | |||
| match = this.tail.substring(0, index); | |||
| this.tail = this.tail.substring(index); | |||
| } | |||
| this.pos += match.length; | |||
| return match; | |||
| }; | |||
| /** | |||
| * Represents a rendering context by wrapping a view object and | |||
| * maintaining a reference to the parent context. | |||
| */ | |||
| function Context(view, parentContext) { | |||
| this.view = view; | |||
| this.cache = { '.': this.view }; | |||
| this.parent = parentContext; | |||
| } | |||
| /** | |||
| * Creates a new context using the given view with this context | |||
| * as the parent. | |||
| */ | |||
| Context.prototype.push = function push(view) { | |||
| return new Context(view, this); | |||
| }; | |||
| /** | |||
| * Returns the value of the given name in this context, traversing | |||
| * up the context hierarchy if the value is absent in this context's view. | |||
| */ | |||
| Context.prototype.lookup = function lookup(name) { | |||
| var cache = this.cache; | |||
| var value; | |||
| if (cache.hasOwnProperty(name)) { | |||
| value = cache[name]; | |||
| } else { | |||
| var context = this, names, index, lookupHit = false; | |||
| while (context) { | |||
| if (name.indexOf('.') > 0) { | |||
| value = context.view; | |||
| names = name.split('.'); | |||
| index = 0; | |||
| /** | |||
| * Using the dot notion path in `name`, we descend through the | |||
| * nested objects. | |||
| * | |||
| * To be certain that the lookup has been successful, we have to | |||
| * check if the last object in the path actually has the property | |||
| * we are looking for. We store the result in `lookupHit`. | |||
| * | |||
| * This is specially necessary for when the value has been set to | |||
| * `undefined` and we want to avoid looking up parent contexts. | |||
| **/ | |||
| while (value != null && index < names.length) { | |||
| if (index === names.length - 1) | |||
| lookupHit = hasProperty(value, names[index]); | |||
| value = value[names[index++]]; | |||
| } | |||
| } else { | |||
| value = context.view[name]; | |||
| lookupHit = hasProperty(context.view, name); | |||
| } | |||
| if (lookupHit) | |||
| break; | |||
| context = context.parent; | |||
| } | |||
| cache[name] = value; | |||
| } | |||
| if (isFunction(value)) | |||
| value = value.call(this.view); | |||
| return value; | |||
| }; | |||
| /** | |||
| * A Writer knows how to take a stream of tokens and render them to a | |||
| * string, given a context. It also maintains a cache of templates to | |||
| * avoid the need to parse the same template twice. | |||
| */ | |||
| function Writer() { | |||
| this.cache = {}; | |||
| } | |||
| /** | |||
| * Clears all cached templates in this writer. | |||
| */ | |||
| Writer.prototype.clearCache = function clearCache() { | |||
| this.cache = {}; | |||
| }; | |||
| /** | |||
| * Parses and caches the given `template` and returns the array of tokens | |||
| * that is generated from the parse. | |||
| */ | |||
| Writer.prototype.parse = function parse(template, tags) { | |||
| var cache = this.cache; | |||
| var tokens = cache[template]; | |||
| if (tokens == null) | |||
| tokens = cache[template] = parseTemplate(template, tags); | |||
| return tokens; | |||
| }; | |||
| /** | |||
| * High-level method that is used to render the given `template` with | |||
| * the given `view`. | |||
| * | |||
| * The optional `partials` argument may be an object that contains the | |||
| * names and templates of partials that are used in the template. It may | |||
| * also be a function that is used to load partial templates on the fly | |||
| * that takes a single argument: the name of the partial. | |||
| */ | |||
| Writer.prototype.render = function render(template, view, partials) { | |||
| var tokens = this.parse(template); | |||
| var context = (view instanceof Context) ? view : new Context(view); | |||
| return this.renderTokens(tokens, context, partials, template); | |||
| }; | |||
| /** | |||
| * Low-level method that renders the given array of `tokens` using | |||
| * the given `context` and `partials`. | |||
| * | |||
| * Note: The `originalTemplate` is only ever used to extract the portion | |||
| * of the original template that was contained in a higher-order section. | |||
| * If the template doesn't use higher-order sections, this argument may | |||
| * be omitted. | |||
| */ | |||
| Writer.prototype.renderTokens = function renderTokens(tokens, context, partials, originalTemplate) { | |||
| var buffer = ''; | |||
| var token, symbol, value; | |||
| for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { | |||
| value = undefined; | |||
| token = tokens[i]; | |||
| symbol = token[0]; | |||
| if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); | |||
| else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); | |||
| else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate); | |||
| else if (symbol === '&') value = this.unescapedValue(token, context); | |||
| else if (symbol === 'name') value = this.escapedValue(token, context); | |||
| else if (symbol === 'text') value = this.rawValue(token); | |||
| if (value !== undefined) | |||
| buffer += value; | |||
| } | |||
| return buffer; | |||
| }; | |||
| Writer.prototype.renderSection = function renderSection(token, context, partials, originalTemplate) { | |||
| var self = this; | |||
| var buffer = ''; | |||
| var value = context.lookup(token[1]); | |||
| // This function is used to render an arbitrary template | |||
| // in the current context by higher-order sections. | |||
| function subRender(template) { | |||
| return self.render(template, context, partials); | |||
| } | |||
| if (!value) return; | |||
| if (isArray(value)) { | |||
| for (var j = 0, valueLength = value.length; j < valueLength; ++j) { | |||
| buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); | |||
| } | |||
| } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { | |||
| buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); | |||
| } else if (isFunction(value)) { | |||
| if (typeof originalTemplate !== 'string') | |||
| throw new Error('Cannot use higher-order sections without the original template'); | |||
| // Extract the portion of the original template that the section contains. | |||
| value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); | |||
| if (value != null) | |||
| buffer += value; | |||
| } else { | |||
| buffer += this.renderTokens(token[4], context, partials, originalTemplate); | |||
| } | |||
| return buffer; | |||
| }; | |||
| Writer.prototype.renderInverted = function renderInverted(token, context, partials, originalTemplate) { | |||
| var value = context.lookup(token[1]); | |||
| // Use JavaScript's definition of falsy. Include empty arrays. | |||
| // See https://github.com/janl/mustache.js/issues/186 | |||
| if (!value || (isArray(value) && value.length === 0)) | |||
| return this.renderTokens(token[4], context, partials, originalTemplate); | |||
| }; | |||
| Writer.prototype.renderPartial = function renderPartial(token, context, partials) { | |||
| if (!partials) return; | |||
| var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; | |||
| if (value != null) | |||
| return this.renderTokens(this.parse(value), context, partials, value); | |||
| }; | |||
| Writer.prototype.unescapedValue = function unescapedValue(token, context) { | |||
| var value = context.lookup(token[1]); | |||
| if (value != null) | |||
| return value; | |||
| }; | |||
| Writer.prototype.escapedValue = function escapedValue(token, context) { | |||
| var value = context.lookup(token[1]); | |||
| if (value != null) | |||
| return mustache.escape(value); | |||
| }; | |||
| Writer.prototype.rawValue = function rawValue(token) { | |||
| return token[1]; | |||
| }; | |||
| mustache.name = 'mustache.js'; | |||
| mustache.version = '2.2.1'; | |||
| mustache.tags = ['{{', '}}']; | |||
| // All high-level mustache.* functions use this writer. | |||
| var defaultWriter = new Writer(); | |||
| /** | |||
| * Clears all cached templates in the default writer. | |||
| */ | |||
| mustache.clearCache = function clearCache() { | |||
| return defaultWriter.clearCache(); | |||
| }; | |||
| /** | |||
| * Parses and caches the given template in the default writer and returns the | |||
| * array of tokens it contains. Doing this ahead of time avoids the need to | |||
| * parse templates on the fly as they are rendered. | |||
| */ | |||
| mustache.parse = function parse(template, tags) { | |||
| return defaultWriter.parse(template, tags); | |||
| }; | |||
| /** | |||
| * Renders the `template` with the given `view` and `partials` using the | |||
| * default writer. | |||
| */ | |||
| mustache.render = function render(template, view, partials) { | |||
| if (typeof template !== 'string') { | |||
| throw new TypeError('Invalid template! Template should be a "string" ' + | |||
| 'but "' + typeStr(template) + '" was given as the first ' + | |||
| 'argument for mustache#render(template, view, partials)'); | |||
| } | |||
| return defaultWriter.render(template, view, partials); | |||
| }; | |||
| // This is here for backwards compatibility with 0.4.x., | |||
| /*eslint-disable */ // eslint wants camel cased function name | |||
| mustache.to_html = function to_html(template, view, partials, send) { | |||
| /*eslint-enable*/ | |||
| var result = mustache.render(template, view, partials); | |||
| if (isFunction(send)) { | |||
| send(result); | |||
| } else { | |||
| return result; | |||
| } | |||
| }; | |||
| // Export the escaping function so that the user may override it. | |||
| // See https://github.com/janl/mustache.js/issues/244 | |||
| mustache.escape = escapeHtml; | |||
| // Export these mainly for testing, but also for advanced usage. | |||
| mustache.Scanner = Scanner; | |||
| mustache.Context = Context; | |||
| mustache.Writer = Writer; | |||
| })); | |||
| @@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LLamaSharp", "LLama\LLamaSh | |||
| EndProject | |||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LLama.WebAPI", "LLama.WebAPI\LLama.WebAPI.csproj", "{D3CEC57A-9027-4DA4-AAAC-612A1EB50ADF}" | |||
| EndProject | |||
| Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLama.Web", "LLama.Web\LLama.Web.csproj", "{C3531DB2-1B2B-433C-8DE6-3541E3620DB1}" | |||
| EndProject | |||
| Global | |||
| GlobalSection(SolutionConfigurationPlatforms) = preSolution | |||
| Debug|Any CPU = Debug|Any CPU | |||
| @@ -69,6 +71,18 @@ Global | |||
| {D3CEC57A-9027-4DA4-AAAC-612A1EB50ADF}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| {D3CEC57A-9027-4DA4-AAAC-612A1EB50ADF}.Release|x64.ActiveCfg = Release|Any CPU | |||
| {D3CEC57A-9027-4DA4-AAAC-612A1EB50ADF}.Release|x64.Build.0 = Release|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Debug|x64.ActiveCfg = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Debug|x64.Build.0 = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.GPU|Any CPU.ActiveCfg = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.GPU|Any CPU.Build.0 = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.GPU|x64.ActiveCfg = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.GPU|x64.Build.0 = Debug|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Release|Any CPU.Build.0 = Release|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Release|x64.ActiveCfg = Release|Any CPU | |||
| {C3531DB2-1B2B-433C-8DE6-3541E3620DB1}.Release|x64.Build.0 = Release|Any CPU | |||
| EndGlobalSection | |||
| GlobalSection(SolutionProperties) = preSolution | |||
| HideSolutionNode = FALSE | |||