diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor index 4873e6c7..13b8d0ca 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/Playground.razor @@ -1,17 +1,22 @@ @page "/playground" @rendermode InteractiveServer +@using AzureOpenAIProxy.PlaygroundApp.Models + Playground Page

Azure OpenAI Proxy Playground

- - - + + + - + @@ -19,6 +24,7 @@ @code { private string? systemMessage; + private ChatParameters? chatParameters; private async Task SetSystemMessage(string systemMessage) { @@ -26,4 +32,11 @@ await Task.CompletedTask; } + + private async Task SetChatParameters(ChatParameters parameters) + { + this.chatParameters = parameters; + + await Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor index fcfeafb7..69e65ef8 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigTabComponent.razor @@ -1,10 +1,12 @@ +@using AzureOpenAIProxy.PlaygroundApp.Models + - This is "Parameters" tab. + @@ -19,6 +21,9 @@ [Parameter] public EventCallback OnSystemMessageChanged { get; set; } + [Parameter] + public EventCallback OnChatParametersChanged { get; set; } + private async Task ChangeTab(FluentTab tab) { this.selectedTab = tab; @@ -29,7 +34,7 @@ private async Task SetSystemMessage(string systemMessage) { this.systemMessage = systemMessage; - + await OnSystemMessageChanged.InvokeAsync(systemMessage); } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigWindowComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigWindowComponent.razor index 739e4e7c..d1155ca5 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigWindowComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ConfigWindowComponent.razor @@ -5,7 +5,10 @@ - + @code { @@ -19,6 +22,9 @@ [Parameter] public EventCallback OnSystemMessageChanged { get; set; } + [Parameter] + public EventCallback OnChatParametersChanged { get; set; } + private async Task SetApiKey(string apiKey) { this.apiKey = apiKey; @@ -36,7 +42,7 @@ private async Task SetSystemMessage(string systemMessage) { this.systemMessage = systemMessage; - + await OnSystemMessageChanged.InvokeAsync(systemMessage); } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterMultiselectComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterMultiselectComponent.razor new file mode 100644 index 00000000..daf212a6 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterMultiselectComponent.razor @@ -0,0 +1,76 @@ +
+ + +
+ + + Create "@(context)" + + +
+
+ +@code { + [Parameter] + public string? Id { get; set; } + + // Label Property + [Parameter, EditorRequired] + public string LabelText { get; set; } = string.Empty; + + [Parameter, EditorRequired] + public string TooltipText { get; set; } = string.Empty; + + // Bind Property + [Parameter] + public IEnumerable? Values { get; set; } + + [Parameter] + public EventCallback> ValuesChanged { get; set; } + + // Fields + private List searchTextItems = new(); + + private async Task OnChangeValues() + { + await ValuesChanged.InvokeAsync(Values); + } + + private void OnSearch(OptionsSearchEventArgs e) + { + searchTextItems.Clear(); + + var input = e.Text.Trim(); + if (string.IsNullOrWhiteSpace(input) || + Values != null && Values.Contains(input)) + { + return; + } + + searchTextItems.Add(input); + e.Items = searchTextItems; + } +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor new file mode 100644 index 00000000..c4bb6782 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor @@ -0,0 +1,103 @@ +
+ + + + + + + + + + + @if (!hasNoError) + { + + @errorText + + } +
+ +@code { + [Parameter] + public string? Id { get; set; } + + // Label Property + [Parameter, EditorRequired] + public string LabelText { get; set; } = string.Empty; + + [Parameter, EditorRequired] + public string TooltipText { get; set; } = string.Empty; + + // Slider Property + [Parameter, EditorRequired] + public float Min { get; set; } = default; + + [Parameter, EditorRequired] + public float Max { get; set; } = default; + + [Parameter, EditorRequired] + public float Step { get; set; } = default; + + // Text Field Property + public string? textFieldValue { get; set; } + + // Bind Property + [Parameter] + public float Value { get; set; } + + [Parameter] + public EventCallback ValueChanged { get; set; } + + // Fields + private bool hasNoError = true; + private string errorText = string.Empty; + + protected override void OnInitialized() + { + textFieldValue = Value!.ToString(); + errorText = $"Only numbers between {Min} and {Max} are permitted"; + + base.OnInitialized(); + } + + private async Task AfterSliderChange() + { + hasNoError = true; + textFieldValue = Value!.ToString(); + + await ValueChanged.InvokeAsync(Value); + } + + private async Task AfterTextFieldChange() + { + hasNoError = float.TryParse(textFieldValue, null, out var parsed); + if (!hasNoError) + return; + + hasNoError = parsed >= Min! && parsed <= Max!; + if (!hasNoError) + return; + + this.Value = parsed; + await ValueChanged.InvokeAsync(Value); + } +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor.css new file mode 100644 index 00000000..16b71e1c --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParameterRangeComponent.razor.css @@ -0,0 +1,24 @@ +::deep .parameter-component-label { + display: inline-block; + padding-left: 5px; +} + +::deep .parameter-component-slider { + width: 88%; + padding-top: 15px; +} + +::deep .parameter-component-textfield { + width: 12%; + font-size: 9px; + padding-right: 5px; +} + +::deep .parameter-component-error { + padding: 10px; + width: 95%; + margin: 5px auto; + border-color: crimson; + background-color: lightcoral; + color: black +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParametersTabComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParametersTabComponent.razor new file mode 100644 index 00000000..eb0c2876 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/ParametersTabComponent.razor @@ -0,0 +1,87 @@ +@using System.Linq + +@using AzureOpenAIProxy.PlaygroundApp.Models + + + @* Past Messages Range *@ + + + @* Max Response Range *@ + + + @* Temperature Range *@ + + + @* Top P Range *@ + + + @* Stop Sequence Multi Select *@ + + + @* Frequency Penalty Range *@ + + + @* Presence Penalty Range *@ + + + +@code { + [Parameter] + public string? Id { get; set; } + + [Parameter] + public EventCallback OnParametersChanged { get; set; } + + public ChatParameters parameters = new(); + + protected override void OnInitialized() + { + base.OnInitialized(); + + parameters.pastMessages = 10; + parameters.maxResponse = 800; + parameters.temperature = 0.7f; + parameters.topP = 0.95f; + parameters.frequencyPenalty = 0; + parameters.presencePenalty = 0; + } + + public async Task HandleParametersChanged() + { + await OnParametersChanged.InvokeAsync(parameters); + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Models/ChatParameters.cs b/src/AzureOpenAIProxy.PlaygroundApp/Models/ChatParameters.cs new file mode 100644 index 00000000..ee1e6b57 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Models/ChatParameters.cs @@ -0,0 +1,14 @@ +namespace AzureOpenAIProxy.PlaygroundApp.Models; + +public class ChatParameters +{ + public float pastMessages; + public float maxResponse; + public float temperature; + public float topP; + + public IEnumerable? stopSequence; + + public float frequencyPenalty; + public float presencePenalty; +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageConfigWindowTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageConfigWindowTests.cs index 2b4a100a..b9271023 100644 --- a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageConfigWindowTests.cs +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaygroundPageConfigWindowTests.cs @@ -358,7 +358,7 @@ string expectedValue await applyButton.ClickAsync(new() { Delay = 500 }); await resetButton.ClickAsync(new() { Delay = 500 }); await Task.Delay(1000); - + var actualValue = await systemMessageTextArea.GetAttributeAsync("value"); var isApplyButtonEnabled = await applyButton.GetAttributeAsync("disabled"); var isResetButtonEnabled = await resetButton.GetAttributeAsync("disabled"); @@ -368,4 +368,95 @@ string expectedValue isApplyButtonEnabled.Should().NotBeNull(); isResetButtonEnabled.Should().NotBeNull(); } -} + + [TestCase("range-past-messages", "Past messages included")] + [TestCase("range-max-response", "Max response")] + [TestCase("range-temperature", "Temperature")] + [TestCase("range-top-p", "Top P")] + [TestCase("range-frequency-penalty", "Frequency penalty")] + [TestCase("range-presence-penalty", "Presence penalty")] + public async Task Given_ParameterTab_When_Updated_Then_Parameter_Slider_Should_Be_Visible(string id, string label) + { + // Arrange + var configTab = Page.Locator("div.config-grid") + .Locator("fluent-tabs#config-tabs"); + await configTab.Locator("fluent-tab#parameters-tab") + .ClickAsync(); + + var component = configTab.Locator("fluent-tab-panel#parameters-tab-panel") + .Locator($"div#{id}"); + var labelComponent = component.Locator($"label[for='{id}-content']"); + + // Act + var labelText = await labelComponent.TextContentAsync(); + var slider = component.Locator($"fluent-slider#{id}-slider"); + var textfield = component.Locator($"fluent-text-field#{id}-textfield"); + + // Assert + labelText.Should().StartWith(label); + await Expect(slider).ToBeVisibleAsync(); + await Expect(textfield).ToBeVisibleAsync(); + } + + [Test] + [TestCase("select-stop-sequence", "Stop sequence")] + public async Task Given_ParameterTab_When_Updated_Then_Parameter_MultiSelect_Should_Be_Visible(string id, string label) + { + // Arrange + var configTab = Page.Locator("div.config-grid") + .Locator("fluent-tabs#config-tabs"); + await configTab.Locator("fluent-tab#parameters-tab") + .ClickAsync(); + + var component = configTab.Locator("fluent-tab-panel#parameters-tab-panel") + .Locator($"div#{id}"); + var labelComponent = component.Locator($"label[for='{id}-content']"); + + // Act + var labelText = await labelComponent.TextContentAsync(); + var multiselect = component.Locator($"fluent-text-field#{id}-textfield"); + + // Assert + labelText.Should().StartWith(label); + await Expect(multiselect).ToBeVisibleAsync(); + } + + [Test] + [TestCase("range-past-messages", 1, 20, 10)] + [TestCase("range-max-response", 1, 16000, 800)] + [TestCase("range-temperature", 0, 1, 0.7)] + [TestCase("range-top-p", 0, 1, 0.95)] + [TestCase("range-frequency-penalty", 0, 2, 0)] + [TestCase("range-presence-penalty", 0, 2, 0)] + public async Task Given_ParameterTab_When_Updated_Then_Parameter_Range_Should_Have_Correct_Range(string id, decimal min, decimal max, decimal start) + { + // Arrange + var configTab = Page.Locator("div.config-grid") + .Locator("fluent-tabs#config-tabs"); + await configTab.Locator("fluent-tab#parameters-tab") + .ClickAsync(); + + var content = configTab.Locator("fluent-tab-panel#parameters-tab-panel") + .Locator($"div#{id}") + .Locator($"div#{id}-content"); + + // Act + var slider = content.Locator("fluent-slider.parameter-component-slider"); + var textfield = content.Locator("fluent-text-field.parameter-component-textfield"); + + var handle = slider.Locator("div.thumb-cursor"); + var bound = await handle.BoundingBoxAsync(); + + // Assert + (await slider.GetAttributeAsync("current-value")).Should().Be(start.ToString()); + (await textfield.GetAttributeAsync("current-value")).Should().Be(start.ToString()); + + await Page.Mouse.DragElementToPoint(handle, 0, bound!.Y); + (await slider.GetAttributeAsync("current-value")).Should().Be(min.ToString()); + (await textfield.GetAttributeAsync("current-value")).Should().Be(min.ToString()); + + await Page.Mouse.DragElementToPoint(handle, float.MaxValue, bound!.Y); + (await slider.GetAttributeAsync("current-value")).Should().Be(max.ToString()); + (await textfield.GetAttributeAsync("current-value")).Should().Be(max.ToString()); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaywrightPageExtensions.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaywrightPageExtensions.cs new file mode 100644 index 00000000..66971155 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/PlaywrightPageExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Playwright; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +internal static class PlaywrightPageExtensions +{ + public static async Task DragElementToPoint(this IMouse mouse, ILocator source, float x, float y) + { + await source.HoverAsync(); + await mouse.DownAsync(); + + // Double execution for reliable mouse move https://playwright.dev/docs/input + await mouse.MoveAsync(x, y); + await mouse.MoveAsync(x, y); + + await mouse.UpAsync(); + } +} \ No newline at end of file