Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in Chat UI to Quickstart Playground #3913

Closed
wants to merge 2 commits into from
Closed

Conversation

chenss3
Copy link
Contributor

@chenss3 chenss3 commented Sep 30, 2024

Summary of the pull request

#3908 with the SDK changes needs to be merged in first
This adds the UI changes required for the chat style portion of Quickstart Playground, where the user can chat with the UI to refine the prompt before they generate a codespace.

References and relevant issues

Detailed description of the pull request / Additional comments

Video demo:
https://github.com/user-attachments/assets/a6753ad4-4cc6-4639-88d6-4dc00093deb8
image## Validation steps performed

PR checklist

  • Closes #xxx
  • Tests added/passed
  • Documentation updated

@chenss3 chenss3 requested a review from manodasanW September 30, 2024 22:49
@chenss3 chenss3 changed the title User/chenss3/chatstyle Add in Chat UI to Quickstart Playground Sep 30, 2024
/// <summary>
/// Converter to convert the PromptStatus bool value to a style that will be used by the GenerateButton.
/// </summary>
public class PromptStatusToGenerateButtonStyle : IValueConverter
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This converter works on a bool and has domain knowledge and "PromptStyle" I suggest to rename the class to something generic BoolToAccentButtonStyle In case other parts of DevHome want to use the converter.

namespace DevHome.Common.Environments.Converters;

/// <summary>
/// Converter to convert the PromptStatus bool value to a style that will be used by the GenerateButton.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converts a PromptStatus... Devs know this is a converter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the other converter.

{
try
{
TelemetryFactory.Get<ITelemetry>().LogCritical("QuickstartPlaygroundCreateChatStyleGenerationOperation");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use .Log as this allows you to add extra information.

{
try
{
TelemetryFactory.Get<ITelemetry>().LogCritical("QuickstartPlaygroundCreateChatStyleGenerationOperation");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow the standard event naming convention. DevHome_[Area]_Event Maybe DevHome_GenerationOperation_Event

catch (Exception ex)
{
TelemetryFactory.Get<ITelemetry>().LogException("QuickstartPlaygroundCreateChatStyleGenerationOperation", ex);
_log.Error(ex, $"CreateProjectGenerationOperation for: {this} failed due to exception");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will {this} print? The name of the class?

@@ -33,7 +33,7 @@ public async Task<List<QuickStartProjectProvider>> GetQuickStartProjectProviders
{
try
{
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider>()).ToList();
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider2>()).ToList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this releases but no extensions implement IQuickStartPRojectProvider2? GetListOfProvidersAsync will return 0 results. This is true even if some providers implement IQuickStartProjectProvider

@@ -33,7 +33,7 @@ public async Task<List<QuickStartProjectProvider>> GetQuickStartProjectProviders
{
try
{
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider>()).ToList();
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider2>()).ToList();
foreach (var quickStartProjectProvider in quickStartProjectProviders)
{
if (quickStartProjectProvider != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can GetListOfProvidersAsync return null values?`

@@ -33,7 +33,7 @@ public async Task<List<QuickStartProjectProvider>> GetQuickStartProjectProviders
{
try
{
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider>()).ToList();
var quickStartProjectProviders = (await extension.GetListOfProvidersAsync<IQuickStartProjectProvider2>()).ToList();
foreach (var quickStartProjectProvider in quickStartProjectProviders)
Copy link
Contributor

@dhoehna dhoehna Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quickStartPRojectProviders.Where(x => x != null);

@@ -1919,6 +1919,14 @@
<value>Generate project</value>
<comment>Tooltip for the generate button on the QuickstartPlayground page</comment>
</data>
<data name="QuickstartPlaygroundSubmitButton.Content" xml:space="preserve">
<value>Submit</value>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use submit Change the name to the action that will happen when the button is clicked.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for "Submit prompt"

{
var message = CustomPrompt;

if (message != null && message != string.Empty)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!string.IsNullOrEmpty(message)


TelemetryFactory.Get<ITelemetry>().Log("QuickstartPlaygroundGenerateButtonClicked", LogLevel.Critical, new GenerateButtonClicked(userPrompt), ActivityId);
// TODO: Replace this (and throughout) with proper diagnostics / logging
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this TODO be addressed in a later PR?

TelemetryFactory.Get<ITelemetry>().Log("QuickstartPlaygroundGenerateButtonClicked", LogLevel.Critical, new GenerateButtonClicked(userPrompt), ActivityId);
// TODO: Replace this (and throughout) with proper diagnostics / logging
ArgumentNullException.ThrowIfNullOrEmpty(userPrompt);
ArgumentNullException.ThrowIfNull(ActiveQuickstartSelection);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can ActivateQuickStartSelection be null if the only way this code runs is when ActivateQuickStartSelection is not null?


TelemetryFactory.Get<ITelemetry>().Log("QuickstartPlaygroundGenerateButtonClicked", LogLevel.Critical, new GenerateButtonClicked(userPrompt), ActivityId);
// TODO: Replace this (and throughout) with proper diagnostics / logging
ArgumentNullException.ThrowIfNullOrEmpty(userPrompt);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question. How can this be null or empty?

// will contain any text from last-opened file in the previous project (which is
// confusing as this project may not have anything to do with the current one). This
// ensures that the file view is back to a known, clean state.
GeneratedFileContent = string.Empty;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The amount of comments for three statements is a lot. Can you reduce this comment, and the once above to one sentence each? An example would be Reset the file content so any previous content is not visible

// TODO: Replace this (and throughout) with proper diagnostics / logging
ArgumentNullException.ThrowIfNullOrEmpty(userPrompt);
ArgumentNullException.ThrowIfNull(ActiveQuickstartSelection);
var userPrompt = CustomPrompt;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why re-assign CustomPrompt?

IProgress<QuickStartProjectProgress> progress = new Progress<QuickStartProjectProgress>(UpdateProgress);
// Temporarily turn off the provider combobox and ensure user cannot edit the prompt for the moment
EnableQuickstartProjectCombobox = false;
IsPromptTextBoxReadOnly = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please stay consistent with what true means.

In this case false will disable two controls and true disables another. Consider renaming IsPromptTextBoxReadOnly in a way where false makes it readonly.

Considering all these controls need to be disabled. Can these three controls be controlled with 1 bool instead of three?

SetUpFileView();
SetupLaunchButton();
EnableProjectButtons = true;
TelemetryFactory.Get<ITelemetry>().LogCritical("QuickstartPlaygroundGenerateSuccceded", relatedActivityId: ActivityId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow event naming conventions.


_quickStartProjectGenerationOperation = ActiveQuickstartSelection.CreateProjectGenerationOperation(userPrompt, outputFolder, ActivityId);
_quickStartProject = await _quickStartProjectGenerationOperation.GenerateAsync().AsTask(progress);
_quickStartProjectGenerationOperation = null!;
Copy link
Contributor

@dhoehna dhoehna Oct 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null!? Why set this to null, then tell the compiler "Trust me. This isn't null."?

else
{
IsErrorViewVisible = true;
ErrorMessage = StringResource.GetLocalized("QuickstartPlaygroundGenerationFailedDetails", _quickStartProject.Result.DisplayMessage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow event naming conventions.

TelemetryFactory.Get<ITelemetry>().Log(
"QuickstartPlaygroundGenerateFailed",
LogLevel.Critical,
new ProjectGenerationErrorInfo(_quickStartProject.Result.DisplayMessage, _quickStartProject.Result.ExtendedError, _quickStartProject.Result.DiagnosticText),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: pass in the exception and have the class break it apart into the data it needs.

}
else
finally
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the exception is not re-thrown the code after the try/catch will always execute. finally isn't needed.

[RelayCommand]
public void CopyExamplePrompt(string selectedPrompt)
{
CustomPrompt = selectedPrompt;
}

public async Task GenerateChatStyleCompetions(string message)
{
if (ActiveQuickstartSelection != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Consider early returns over indentation.

Type = ChatStyleMessage.ChatMessageItemType.Response,
});

if (result.ChatResponse.ToString().Contains("great prompt"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does great prompt need to be localized?


if (result.ChatResponse.ToString().Contains("great prompt"))
{
IsPromptValid = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about a great prompt makes the prompt valid? Additionally, I remember in the SDK you talking about what makes a valid prompt.

Who is responsible for determining if a prompt is valid? Extensions, or DevHome?


ChatMessages.Add(new ChatStyleMessage
{
Name = "Hello! What kind of project would you like to create? Try a sample prompt or write one of your own.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localization.


ChatMessages.Add(new ChatStyleMessage
{
Name = "If you would like to help us guide you to a good prompt, hit the Enter/Return key on your keyboard or press Submit after typing in your prompt. Otherwise, simply click on Generate to ignore prompt guidance.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Localization.

I don't know if "simply" can be used. Some people might not find clicking on "generate" to be a simple task.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: "ignore" is a bit harsh. Otherwise, click on Generate seems good enough.

@@ -699,3 +784,80 @@ public DataTemplate? FileTemplate
return explorerItem.Type == ExplorerItem.ExplorerItemType.Folder ? FolderTemplate : FileTemplate;
}
}

public class ChatStyleMessage : INotifyPropertyChanged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatStyleMessage seems like a model. Not a view model.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this truly is a model. Please remove the observable collections.

@@ -699,3 +784,80 @@ public DataTemplate? FileTemplate
return explorerItem.Type == ExplorerItem.ExplorerItemType.Folder ? FolderTemplate : FileTemplate;
}
}

public class ChatStyleMessage : INotifyPropertyChanged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One class to one file please.

<SolidColorBrush Color="WhiteSmoke" />
</StackPanel.Background>
<Border Padding="8,4,8,4"
CornerRadius="4">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of spaces. Please format the XAML to the correct tab level.

@@ -49,6 +50,82 @@
<ResourceDictionary Source="ms-appx:///DevHome.SetupFlow/Styles/SetupFlowStyles.xaml" />
<ResourceDictionary Source="ms-appx:///DevHome.SetupFlow/Styles/QuickstartStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="ChatResponseMessageTemplate" x:DataType="viewmodels:ChatStyleMessage">
<Grid Height="Auto"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the Height, Margin, and HorizontalAlignment a property of the template, or the parent controller? THese seems like the parent controller wants the margin of the template to be 4, and the height to be auto.

@@ -49,6 +50,82 @@
<ResourceDictionary Source="ms-appx:///DevHome.SetupFlow/Styles/SetupFlowStyles.xaml" />
<ResourceDictionary Source="ms-appx:///DevHome.SetupFlow/Styles/QuickstartStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="ChatResponseMessageTemplate" x:DataType="viewmodels:ChatStyleMessage">
<Grid Height="Auto"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use RowSpacing to control the space between items instead of using margin

<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grid.Row starts at 0. Why is the first item at 1?

</Border.Background>
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock MaxWidth="400"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lemme get this straight.

A TextBlock inside of a StackPanel inside of a Border inside of a StackPanel inside of a grid. Why not a lone TextBlock?

CornerRadius="4">
<Border.Background>
<SolidColorBrush Opacity="0.2"
Color="WhiteSmoke" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please us ThemeResource for opacity and Color so the UI can change to events. For example, switching from dark mode to light mode.

</StackPanel>
</Grid>
</DataTemplate>
<DataTemplate x:Key="ChatRequestMessageTemplate" x:DataType="viewmodels:ChatStyleMessage">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These look identical. What is the different between this, and ChatResponseMessageTemplate?

@@ -100,6 +172,30 @@
TabIndex="1"/>
</ctcontrols:WrapPanel>
</Grid>
<ScrollViewer
VerticalScrollBarVisibility="Auto"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spacing.

Margin="10,0,10,10"
Padding="10"
Background="white"
MaxHeight="300">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why MaxHeigth? Something wrong with the control expanding with the UI?

MinHeight="200"
ItemsSource="{x:Bind ViewModel.ChatMessages, Mode=OneWay}"
ItemTemplateSelector="{StaticResource ChatMessageTemplateSelector}"
IsItemClickEnabled="False"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe IsItemClickEnabled is false when SelectionMode is none, ya?

@@ -129,8 +220,8 @@
PlaceholderText="{x:Bind ViewModel.PromptTextBoxPlaceholder, Mode=OneWay}"
x:Name="CustomPrompt"
TextWrapping="Wrap"
AcceptsReturn="True"
MinWidth="800"
KeyDown="CustomPrompt_KeyDown"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering CustomPrompt_KeyDown calls a method on the ViewModel, consider using a `[RelayCommand] to bypass code-behind.

@@ -168,10 +259,21 @@
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 0 10 0">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use Spacing instead of margin to control the space between child elements.


private async void CustomPrompt_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider early return over indentation.

@chenss3 chenss3 closed this Oct 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants