mirror of
				https://github.com/MindWorkAI/AI-Studio.git
				synced 2025-11-04 11:40:21 +00:00 
			
		
		
		
	Refactor basic assistant concepts into a base component
This commit is contained in:
		
							parent
							
								
									85e429b9ee
								
							
						
					
					
						commit
						f5cb574be8
					
				
							
								
								
									
										39
									
								
								app/MindWork AI Studio/Components/AssistantBase.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/MindWork AI Studio/Components/AssistantBase.razor
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,39 @@
 | 
			
		||||
@using AIStudio.Chat
 | 
			
		||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
 | 
			
		||||
    @this.Title
 | 
			
		||||
</MudText>
 | 
			
		||||
 | 
			
		||||
<InnerScrolling HeaderHeight="12.3em">
 | 
			
		||||
    <ChildContent>
 | 
			
		||||
        <MudForm @ref="@this.form" @bind-IsValid="@this.inputIsValid" @bind-Errors="@this.inputIssues" Class="pr-2">
 | 
			
		||||
            <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
 | 
			
		||||
                @this.Description
 | 
			
		||||
            </MudText>
 | 
			
		||||
 | 
			
		||||
            @if (this.Body is not null)
 | 
			
		||||
            {
 | 
			
		||||
                @this.Body
 | 
			
		||||
            }
 | 
			
		||||
        </MudForm>
 | 
			
		||||
 | 
			
		||||
        @if (this.inputIssues.Any())
 | 
			
		||||
        {
 | 
			
		||||
            <MudPaper Class="pr-2 mt-3" Outlined="@true">
 | 
			
		||||
                <MudText Typo="Typo.h6">Issues</MudText>
 | 
			
		||||
                <MudList Clickable="@true">
 | 
			
		||||
                    @foreach (var issue in this.inputIssues)
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
 | 
			
		||||
                            @issue
 | 
			
		||||
                        </MudListItem>
 | 
			
		||||
                    }
 | 
			
		||||
                </MudList>
 | 
			
		||||
            </MudPaper>
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        @if (this.resultingContentBlock is not null)
 | 
			
		||||
        {
 | 
			
		||||
            <ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
 | 
			
		||||
        }
 | 
			
		||||
    </ChildContent>
 | 
			
		||||
</InnerScrolling>
 | 
			
		||||
							
								
								
									
										104
									
								
								app/MindWork AI Studio/Components/AssistantBase.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								app/MindWork AI Studio/Components/AssistantBase.razor.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,104 @@
 | 
			
		||||
using AIStudio.Chat;
 | 
			
		||||
using AIStudio.Provider;
 | 
			
		||||
using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Components;
 | 
			
		||||
 | 
			
		||||
public abstract partial class AssistantBase : ComponentBase
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected SettingsManager SettingsManager { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    protected Random RNG { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    protected string Title { get; init; } = string.Empty;
 | 
			
		||||
    
 | 
			
		||||
    protected string Description { get; init; } = string.Empty;
 | 
			
		||||
    
 | 
			
		||||
    protected abstract string SystemPrompt { get; }
 | 
			
		||||
    
 | 
			
		||||
    private protected virtual RenderFragment? Body => null;
 | 
			
		||||
 | 
			
		||||
    protected AIStudio.Settings.Provider selectedProvider;
 | 
			
		||||
    protected MudForm? form;
 | 
			
		||||
    protected bool inputIsValid;
 | 
			
		||||
    
 | 
			
		||||
    private ChatThread? chatThread;
 | 
			
		||||
    private ContentBlock? resultingContentBlock;
 | 
			
		||||
    private string[] inputIssues = [];
 | 
			
		||||
    
 | 
			
		||||
    #region Overrides of ComponentBase
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
    {
 | 
			
		||||
        // Reset the validation when not editing and on the first render.
 | 
			
		||||
        // We don't want to show validation errors when the user opens the dialog.
 | 
			
		||||
        if(firstRender)
 | 
			
		||||
            this.form?.ResetValidation();
 | 
			
		||||
        
 | 
			
		||||
        await base.OnAfterRenderAsync(firstRender);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    
 | 
			
		||||
    protected void CreateChatThread()
 | 
			
		||||
    {
 | 
			
		||||
        this.chatThread = new()
 | 
			
		||||
        {
 | 
			
		||||
            WorkspaceId = Guid.Empty,
 | 
			
		||||
            ChatId = Guid.NewGuid(),
 | 
			
		||||
            Name = string.Empty,
 | 
			
		||||
            Seed = this.RNG.Next(),
 | 
			
		||||
            SystemPrompt = this.SystemPrompt,
 | 
			
		||||
            Blocks = [],
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    protected DateTimeOffset AddUserRequest(string request)
 | 
			
		||||
    {
 | 
			
		||||
        var time = DateTimeOffset.Now;
 | 
			
		||||
        this.chatThread!.Blocks.Add(new ContentBlock
 | 
			
		||||
        {
 | 
			
		||||
            Time = time,
 | 
			
		||||
            ContentType = ContentType.TEXT,
 | 
			
		||||
            Role = ChatRole.USER,
 | 
			
		||||
            Content = new ContentText
 | 
			
		||||
            {
 | 
			
		||||
                Text = request,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        return time;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async Task AddAIResponseAsync(DateTimeOffset time)
 | 
			
		||||
    {
 | 
			
		||||
        var aiText = new ContentText
 | 
			
		||||
        {
 | 
			
		||||
            // We have to wait for the remote
 | 
			
		||||
            // for the content stream: 
 | 
			
		||||
            InitialRemoteWait = true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.resultingContentBlock = new ContentBlock
 | 
			
		||||
        {
 | 
			
		||||
            Time = time,
 | 
			
		||||
            ContentType = ContentType.TEXT,
 | 
			
		||||
            Role = ChatRole.AI,
 | 
			
		||||
            Content = aiText,
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        this.chatThread?.Blocks.Add(this.resultingContentBlock);
 | 
			
		||||
        
 | 
			
		||||
        // Use the selected provider to get the AI response.
 | 
			
		||||
        // By awaiting this line, we wait for the entire
 | 
			
		||||
        // content to be streamed.
 | 
			
		||||
        await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								app/MindWork AI Studio/Components/AssistantBaseCore.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/MindWork AI Studio/Components/AssistantBaseCore.cs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Rendering;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Components;
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
public abstract class AssistantBaseCore : AssistantBase
 | 
			
		||||
{
 | 
			
		||||
    private protected sealed override RenderFragment Body => this.BuildRenderTree;
 | 
			
		||||
 | 
			
		||||
    // Allow content to be provided by a .razor file but without 
 | 
			
		||||
    // overriding the content of the base class
 | 
			
		||||
    protected new virtual void BuildRenderTree(RenderTreeBuilder builder)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -1,68 +1,29 @@
 | 
			
		||||
@page "/assistant/icons"
 | 
			
		||||
@using AIStudio.Chat
 | 
			
		||||
@using AIStudio.Settings
 | 
			
		||||
@inherits AssistantBaseCore
 | 
			
		||||
 | 
			
		||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
 | 
			
		||||
    Icon Finder
 | 
			
		||||
</MudText>
 | 
			
		||||
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.TextFields" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3"/>
 | 
			
		||||
 | 
			
		||||
<InnerScrolling HeaderHeight="12.3em">
 | 
			
		||||
    <ChildContent>
 | 
			
		||||
        <MudForm @ref="@this.form" @bind-IsValid="@this.inputIsValid" @bind-Errors="@this.inputIssues" Class="pr-2">
 | 
			
		||||
            <MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
 | 
			
		||||
                Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
 | 
			
		||||
                You need to extract a concept from your context, such as from a text. Let's take an example where
 | 
			
		||||
                your text contains statements about multiple departments. The sought-after concept could be "departments."
 | 
			
		||||
                The next challenge is that we need to anticipate the bias of the icon designers: under the search term
 | 
			
		||||
                "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
 | 
			
		||||
                it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
 | 
			
		||||
            </MudText>
 | 
			
		||||
 | 
			
		||||
            <MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.TextFields" Adornment="Adornment.Start" Label="Yout context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3"/>
 | 
			
		||||
 | 
			
		||||
            <MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
 | 
			
		||||
                <MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
 | 
			
		||||
                    @foreach (var source in Enum.GetValues<IconSources>())
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudSelectItem Value="@source">@source.Name()</MudSelectItem>
 | 
			
		||||
                    }
 | 
			
		||||
                </MudSelect>
 | 
			
		||||
                @if (this.selectedIconSource is not IconSources.GENERIC)
 | 
			
		||||
                {
 | 
			
		||||
                    <MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
 | 
			
		||||
                }
 | 
			
		||||
            </MudStack>
 | 
			
		||||
            
 | 
			
		||||
            <MudSelect T="Provider" @bind-Value="@this.selectedProvider" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
 | 
			
		||||
                @foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
 | 
			
		||||
                {
 | 
			
		||||
                    <MudSelectItem Value="@provider"/>
 | 
			
		||||
                }
 | 
			
		||||
            </MudSelect>
 | 
			
		||||
 | 
			
		||||
            <MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
 | 
			
		||||
                Find icons
 | 
			
		||||
            </MudButton>
 | 
			
		||||
        </MudForm>
 | 
			
		||||
 | 
			
		||||
        @if (this.inputIssues.Any())
 | 
			
		||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
 | 
			
		||||
    <MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
 | 
			
		||||
        @foreach (var source in Enum.GetValues<IconSources>())
 | 
			
		||||
        {
 | 
			
		||||
            <MudPaper Class="pr-2 mt-3" Outlined="@true">
 | 
			
		||||
                <MudText Typo="Typo.h6">Issues</MudText>
 | 
			
		||||
                <MudList Clickable="@true">
 | 
			
		||||
                    @foreach (var issue in this.inputIssues)
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
 | 
			
		||||
                            @issue
 | 
			
		||||
                        </MudListItem>
 | 
			
		||||
                    }
 | 
			
		||||
                </MudList>
 | 
			
		||||
            </MudPaper>
 | 
			
		||||
            <MudSelectItem Value="@source">@source.Name()</MudSelectItem>
 | 
			
		||||
        }
 | 
			
		||||
    </MudSelect>
 | 
			
		||||
    @if (this.selectedIconSource is not IconSources.GENERIC)
 | 
			
		||||
    {
 | 
			
		||||
        <MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
 | 
			
		||||
    }
 | 
			
		||||
</MudStack>
 | 
			
		||||
 | 
			
		||||
        @if (this.resultingContentBlock is not null)
 | 
			
		||||
        {
 | 
			
		||||
            <ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
 | 
			
		||||
        }
 | 
			
		||||
    </ChildContent>
 | 
			
		||||
</InnerScrolling>
 | 
			
		||||
<MudSelect T="Provider" @bind-Value="@this.selectedProvider" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
 | 
			
		||||
    @foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
 | 
			
		||||
    {
 | 
			
		||||
        <MudSelectItem Value="@provider"/>
 | 
			
		||||
    }
 | 
			
		||||
</MudSelect>
 | 
			
		||||
 | 
			
		||||
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
 | 
			
		||||
    Find icon
 | 
			
		||||
</MudButton>
 | 
			
		||||
@ -1,45 +1,26 @@
 | 
			
		||||
using AIStudio.Chat;
 | 
			
		||||
using AIStudio.Provider;
 | 
			
		||||
using AIStudio.Settings;
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
 | 
			
		||||
namespace AIStudio.Components.Pages.IconFinder;
 | 
			
		||||
 | 
			
		||||
public partial class AssistantIconFinder : ComponentBase
 | 
			
		||||
public partial class AssistantIconFinder : AssistantBaseCore
 | 
			
		||||
{
 | 
			
		||||
    [Inject]
 | 
			
		||||
    private SettingsManager SettingsManager { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public IJSRuntime JsRuntime { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [Inject]
 | 
			
		||||
    public Random RNG { get; set; } = null!;
 | 
			
		||||
    
 | 
			
		||||
    private ChatThread? chatThread;
 | 
			
		||||
    private ContentBlock? resultingContentBlock;
 | 
			
		||||
    private AIStudio.Settings.Provider selectedProvider;
 | 
			
		||||
    private MudForm form = null!;
 | 
			
		||||
    private bool inputIsValid;
 | 
			
		||||
    private string[] inputIssues = [];
 | 
			
		||||
    private string inputContext = string.Empty;
 | 
			
		||||
    private IconSources selectedIconSource;
 | 
			
		||||
    
 | 
			
		||||
    #region Overrides of ComponentBase
 | 
			
		||||
 | 
			
		||||
    protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
    public AssistantIconFinder()
 | 
			
		||||
    {
 | 
			
		||||
        // Reset the validation when not editing and on the first render.
 | 
			
		||||
        // We don't want to show validation errors when the user opens the dialog.
 | 
			
		||||
        if(firstRender)
 | 
			
		||||
            this.form.ResetValidation();
 | 
			
		||||
        
 | 
			
		||||
        await base.OnAfterRenderAsync(firstRender);
 | 
			
		||||
        this.Title = "Icon Finder";
 | 
			
		||||
        this.Description =
 | 
			
		||||
            """
 | 
			
		||||
            Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
 | 
			
		||||
            You need to extract a concept from your context, such as from a text. Let's take an example where
 | 
			
		||||
            your text contains statements about multiple departments. The sought-after concept could be "departments."
 | 
			
		||||
            The next challenge is that we need to anticipate the bias of the icon designers: under the search term
 | 
			
		||||
            "departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
 | 
			
		||||
            it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
 | 
			
		||||
            """;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    #endregion
 | 
			
		||||
 | 
			
		||||
    private string? ValidatingContext(string context)
 | 
			
		||||
    {
 | 
			
		||||
        if(string.IsNullOrWhiteSpace(context))
 | 
			
		||||
@ -58,72 +39,24 @@ public partial class AssistantIconFinder : ComponentBase
 | 
			
		||||
 | 
			
		||||
    private async Task FindIcon()
 | 
			
		||||
    {
 | 
			
		||||
        await this.form.Validate();
 | 
			
		||||
        await this.form!.Validate();
 | 
			
		||||
        if (!this.inputIsValid)
 | 
			
		||||
            return;
 | 
			
		||||
        
 | 
			
		||||
        //
 | 
			
		||||
        // Create a new chat thread:
 | 
			
		||||
        //
 | 
			
		||||
        this.chatThread = new()
 | 
			
		||||
        {
 | 
			
		||||
            WorkspaceId = Guid.Empty,
 | 
			
		||||
            ChatId = Guid.NewGuid(),
 | 
			
		||||
            Name = string.Empty,
 | 
			
		||||
            Seed = this.RNG.Next(),
 | 
			
		||||
            SystemPrompt = SYSTEM_PROMPT,
 | 
			
		||||
            Blocks = [],
 | 
			
		||||
        };
 | 
			
		||||
        this.CreateChatThread();
 | 
			
		||||
        var time = this.AddUserRequest(
 | 
			
		||||
        $"""
 | 
			
		||||
            {this.selectedIconSource.Prompt()} I search for an icon for the following context:
 | 
			
		||||
            
 | 
			
		||||
        //
 | 
			
		||||
        // Add the user's request to the thread:
 | 
			
		||||
        //
 | 
			
		||||
        var time = DateTimeOffset.Now;
 | 
			
		||||
        this.chatThread.Blocks.Add(new ContentBlock
 | 
			
		||||
        {
 | 
			
		||||
            Time = time,
 | 
			
		||||
            ContentType = ContentType.TEXT,
 | 
			
		||||
            Role = ChatRole.USER,
 | 
			
		||||
            Content = new ContentText
 | 
			
		||||
            {
 | 
			
		||||
                Text =
 | 
			
		||||
                $"""
 | 
			
		||||
                   {this.selectedIconSource.Prompt()} I search for an icon for the following context:
 | 
			
		||||
            ```
 | 
			
		||||
            {this.inputContext}
 | 
			
		||||
            ```
 | 
			
		||||
         """);
 | 
			
		||||
 | 
			
		||||
                   ```
 | 
			
		||||
                   {this.inputContext}
 | 
			
		||||
                   ```
 | 
			
		||||
                """,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        //
 | 
			
		||||
        // Add the AI response to the thread:
 | 
			
		||||
        //
 | 
			
		||||
        var aiText = new ContentText
 | 
			
		||||
        {
 | 
			
		||||
            // We have to wait for the remote
 | 
			
		||||
            // for the content stream: 
 | 
			
		||||
            InitialRemoteWait = true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        this.resultingContentBlock = new ContentBlock
 | 
			
		||||
        {
 | 
			
		||||
            Time = time,
 | 
			
		||||
            ContentType = ContentType.TEXT,
 | 
			
		||||
            Role = ChatRole.AI,
 | 
			
		||||
            Content = aiText,
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        this.chatThread?.Blocks.Add(this.resultingContentBlock);
 | 
			
		||||
        
 | 
			
		||||
        // Use the selected provider to get the AI response.
 | 
			
		||||
        // By awaiting this line, we wait for the entire
 | 
			
		||||
        // content to be streamed.
 | 
			
		||||
        await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
 | 
			
		||||
        await this.AddAIResponseAsync(time);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private const string SYSTEM_PROMPT = 
 | 
			
		||||
    protected override string SystemPrompt => 
 | 
			
		||||
        """
 | 
			
		||||
        I can search for icons using US English keywords. Please help me come up with the right search queries.
 | 
			
		||||
        I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user