Skip to content

a .NET WPF/WinUI Control to easily add an integrated AI assistant to any application

Notifications You must be signed in to change notification settings

mitchcapper/PilotAIAssistantControl

Repository files navigation

PilotAiAssistControl

A WPF (and beta WinUI3) user control that provides an AI chat assistant interface with support for multiple AI providers (GitHub Copilot, Google Gemini, Anthropic, OpenAI, GitHub Models, and custom or local providers like ollama that support OpenAI API endpoints). It is meant to be a very quick way to bolt on an AI agent to any application you have. You can provide context and direct action buttons inline with the AI responses. It includes both a standalone chat control and an expandable sidebar version. It has integrated support for discovery of local existing GitHub Copilot tokens (ie from vscode) and also integrates a login flow for users that don't already have it present.

The included Expandable control allows it to easily collapse down to 40 pixels when not used. It has built in configuration UI for easy setup.

You can optionally provide a "reference text/document" which can be a longer document the user is working on and allow the user to control how much(if any) of it to send along.

Screenshots

In this screenshot it is the left panel Config Pane Collapsed View
Main Screenshot Config Screenshot Collapsed Column

Installation

Install-Package PilotAiAssistControlWPF

Quick Start

Add Namespace

xmlns:pia="clr-namespace:PilotAIAssistantControl;assembly=PilotAIAssistantControl"

Create Your Options Class

Create a class inheriting from AIOptions to configure the AI behavior:

public class RegexAIOptions : AIOptions {
    public override string GetSystemPrompt() =>
        "You are a C# Regex expert assistant. " +
        "Provide regex patterns inside Markdown code blocks (```regex ... ```). " +
        "Explain how the pattern works briefly.";
}

Embed the Control

Option A: Expandable Panel (Recommended for sidebars)

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition x:Name="AiColumn" Width="Auto"/>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <pia:UCAIExpandable x:Name="ucAiExpandable"
                        TargetColumn="{Binding ElementName=AiColumn}"
                        DefaultSize="400"
                        MinExpandedSize="200"
                        CollapsedSize="40"/>

    <GridSplitter Grid.Column="1" HorizontalAlignment="Stretch"/>

    <!-- Your main content in Grid.Column="2" -->
</Grid>

Option B: Direct Control (For fixed layouts)

<pia:UCAI x:Name="ucAi" Width="400"/>

Initialize in Code-Behind

Important: You must call Configure() followed by ImportData() for the control to fully initialize. Always call ImportData() even if you have no data to restore (pass null).

private void Window_Loaded(object sender, RoutedEventArgs e) {
    var options = new RegexAIOptions();

    // For expandable version:
    ucAiExpandable.AiControl.Configure(options);
    ucAiExpandable.AiControl.ImportData(null); // Required even with no saved data

    // Or for direct control:
    ucAi.Configure(options);
    ucAi.ImportData(null);
}

Requirements

  • .NET 6.0+ or .NET Framework 4.7.2+
  • WPF application
  • Newtonsoft.Json (for settings serialization)

Saving and Restoring User Settings

The control supports saving and restoring the user's AI provider configuration. You must call ImportData even if you are not storing any settings (just pass null) as the control does not initialize until you do. You can call ExportData/ImportData to save/restore settings. It will return a Serializable friendly object from ExportData and expects that object back for importing. You can append this into your own serializable config if so desired.

Security: User tokens can be saved in this data for their AI providers depending on the provider. If the token is saved it is protected with the microsoft DPAPI which locks it to the current user account. You should not need to do anything the control will handle it all.

Here is a simple example:

// Save settings (e.g., on window closing)
private void Window_Closing(object sender, CancelEventArgs e) {
    AIUserConfig data = ucAiExpandable.AiControl.ExportData();
    string json = JsonConvert.SerializeObject(data);
    File.WriteAllText("ai_settings.json", json);
}

// Restore settings (on load)
private void Window_Loaded(object sender, RoutedEventArgs e) {
    var options = new RegexAIOptions();
    ucAiExpandable.AiControl.Configure(options);

    AIUserConfig? savedData = null;
    if (File.Exists("ai_settings.json")) {
        savedData = JsonConvert.DeserializeObject<AIUserConfig>(
            File.ReadAllText("ai_settings.json"));
    }
    ucAiExpandable.AiControl.ImportData(savedData);
}

AIOptions Configuration

The AIOptions class controls how the AI chat behaves. Override properties and methods to customize:

Member Description
string GetSystemPrompt() Required. Returns the system prompt that defines the AI's behavior and context.
IAIModelProvider[] Providers Array of available AI providers. Defaults to built-in providers (GitHub Copilot, OpenAI, etc.).
REFERENCE_TEXT_REPLACE_ACTION ReplaceAction Controls how reference text updates are handled in chat history. Default: ChangeOldToPlaceholder.
stirng GetCurrentReferenceText() Returns the current reference text to send with queries (e.g., document content).
string ReferenceTextHeader Header shown to the AI for reference text blocks. Default: "Reference Text".
string ReferenceTextDisplayName User-facing name for reference text. Default: "Reference Text".
int DefaultMaxReferenceTextCharsToSend Max characters of reference text to include. Default: 5000.
bool AllowUserToSetMaxReferenceTextCharsToSend Show UI to let user adjust max chars. Default: true.
String HintForUserInput Placeholder text for the chat input box. Default: "Ask your question here...".
String FormatUserQuestion(string) Transform user questions before sending to AI.
IEnumerable<CodeblockAction> CodeblockActions Custom actions shown on code blocks in AI responses.
void HandleDebugMessage(string) Receive debug messages from the AI service.

It may be non-obvious but if you want to disable the "reference text" functionality you should set ReplaceAction = ReferenceTextDisabled this will disable it completely and hide those UI Components. See below for more details on options.

Reference Text Replace Actions

When using reference text that changes during the conversation we likely don't want to include all the past versions to minimize the context window. The enum value here controls how we handle that while trying to minimize any confusion on the ai model's side as to what happened in the conversation.

Action Behavior
ReferenceTextDisabled Disables reference text feature entirely.
ChangeOldToPlaceholder Replaces old reference text with a placeholder note. (Recommended)
LeaveOldInplace Keeps old versions in history (uses more tokens).
UpdateInPlace Updates reference text where it first appeared.
DeleteOld Removes old reference text completely.

Code Block Actions

When the AI responds with code blocks (markdown fenced code), you can add action buttons that conditionally appear below each block. This lets users quickly apply, copy, or process the AI's code suggestions.

Built-in Actions

  • GenericCodeblockAction.ClipboardAction - A pre-built "πŸ“‹ Copy" sample action that copies the code to clipboard.

Creating Custom Actions

YOu can create your own custom actions either implementing the ICodeblockAction interface directly or using the GenericCodeblockAction helper base class to create custom actions:

new GenericCodeblockAction("πŸ“ Use as Pattern", async (block) => {
    // block.Code contains the code block content
    // block.Language contains the language identifier (e.g., "regex", "csharp")
    _mainWindow.txtPattern.Text = block.Code;
    return true; // Return true on success, false on failure
}) {
    Tooltip = "Use this code block as the regex pattern",
    FeedbackOnAction = "βœ“ Applied!"  // Shown briefly after clicking
}

ICodeBlock Interface

The block parameter passed to your action implements ICodeBlock:

Property Type Description
Code string The content of the code block (without the fences).
Language string The language identifier from the fence (e.g., regex, csharp, json).

Conditional Visibility

To show actions only for specific code block types, set IsVisibleDel. It will get the entire ICodeBlock object to determine if it wants to offer its action:

new GenericCodeblockAction("πŸ“ Use as Pattern", async (block) => {
    _mainWindow.txtPattern.Text = block.Code;
    return true;
}) {
    Tooltip = "Apply this regex pattern",
    FeedbackOnAction = "βœ“ Applied!",
    IsVisibleDel = (block) => block.Language == "regex"  // Only show for regex blocks
}

Implementing the CodeblockAction Interface

For more control, implement the CodeblockAction interface directly:

public interface ICodeblockAction {
    Task<bool> DoAction(ICodeBlock block);  // Execute the action
    string DisplayName { get; }              // Button text (e.g., "πŸ“‹ Copy")
    string Tooltip => DisplayName;           // Hover tooltip
    string FeedbackOnAction => "βœ“ Done!";    // Shown after action completes
    bool IsVisible(ICodeBlock block) => true; // Control visibility per block
}

Example: Full Options Class

public class RegexAIOptions : AIOptions {
    private readonly MainWindow _mainWindow;

    public RegexAIOptions(MainWindow mainWindow) {
        _mainWindow = mainWindow;
    }

    public override string GetSystemPrompt() =>
        "You are a C# Regex expert assistant. The user has questions about their regex patterns and target text. " +
        "Provide regex patterns inside Markdown code blocks (```regex ... ```). " +
        "Explain how the pattern works briefly. " +
        "If the language supports named capture groups, use these by default.";

    public override string GetCurrentReferenceText() =>
        _mainWindow.txtTargetText.Text;

    public override string FormatUserQuestion(string userQuestion) =>
        $"Current pattern:\n```regex\n{_mainWindow.txtPattern.Text}\n```\n\nMy question: {userQuestion}";

    public override string ReferenceTextHeader => "Users current target text";
    public override string ReferenceTextDisplayName => "Target Text";
    public override string HintForUserInput => "Ask about a pattern or matching...";
    public override int DefaultMaxReferenceTextCharsToSend => 5000;

    public override IEnumerable<CodeblockAction> CodeblockActions => [
        GenericCodeblockAction.ClipboardAction,
        new GenericCodeblockAction("πŸ“ Use as Pattern", async (block) => {
            _mainWindow.txtPattern.Text = block.Code;
            return true;
        }) {
            Tooltip = "Use this code block as the regex pattern",
            FeedbackOnAction = "βœ“ Applied!"
        }
    ];

    public override void HandleDebugMessage(string msg) =>
        System.Diagnostics.Debug.WriteLine(msg);
}

UCAIExpandable Properties

Property Type Description
TargetColumn ColumnDefinition Grid column to resize on expand/collapse (horizontal mode).
TargetRow RowDefinition Grid row to resize on expand/collapse (vertical mode).
DefaultSize double Initial expanded size. Default: 400.
MinExpandedSize double Minimum size when expanded. Default: 200.
CollapsedSize double Size when collapsed. Default: 40.
IsExpanded bool Current expansion state (two-way bindable).
Header object Custom header content for the expander.
AiControl UCAI Access to the inner AI chat control.
ExpanderControl Expander Access to the inner Expander control.

Multiple Agents / System Prompts

The control doesn't have built-in UI for switching between multiple AI "agents" (different system prompts), but you can easily implement this yourself by calling Configure() with different AIOptions instances. If the "Providers" property of the AIOptions is the same between AiOption instances then the user's provider settings are preserved across agent switches.

Example: Agent Switcher

Define multiple agents with different system prompts:

// Agent 1: Generates regex patterns based on user requirements
public class RegexGeneratorAgent : AIOptions {
    public override string GetSystemPrompt() =>
        "You are a C# Regex expert. Help users create regex patterns. " +
        "Provide patterns in ```regex code blocks.";

    public override string HintForUserInput => "Describe what you want to match...";
    public override string ReferenceTextHeader => "Users current target text";
    public override string GetCurrentReferenceText() => MainWindow.Instance.txtTest.Text;

    public override string FormatUserQuestion(string userQuestion) =>
        $"Current pattern:\n```regex\n{MainWindow.Instance.txtRegex.Text}\n```\n\n{userQuestion}";
}

// Agent 2: Explains existing regex patterns
public class RegexExplainerAgent : AIOptions {
    public override string GetSystemPrompt() =>
        "You are a regex expert. Explain how regex patterns work. " +
        "Be concise - a few sentences for simple patterns, a paragraph for complex ones. " +
        "Don't give examples unless asked.";

    public override string HintForUserInput => "Provide a regex pattern to explain...";

    public override string FormatUserQuestion(string userQuestion) =>
        $"Please explain this regex:\n```regex\n{userQuestion}\n```";

    // This agent doesn't need reference text
    public override REFERENCE_TEXT_REPLACE_ACTION ReplaceAction =>
        REFERENCE_TEXT_REPLACE_ACTION.ReferenceTextDisabled;
}

Add a ComboBox above the AI pane to switch agents:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>

    <ComboBox x:Name="agentCombo" SelectionChanged="AgentCombo_SelectionChanged">
        <ComboBoxItem Content="Regex Generator" IsSelected="True"/>
        <ComboBoxItem Content="Regex Explainer"/>
    </ComboBox>

    <pia:UCAIExpandable Grid.Row="1" x:Name="ucAi"
                        TargetColumn="{Binding ElementName=AiColumn}"/>
</Grid>

Handle the switch in code-behind:

private RegexGeneratorAgent _generatorAgent = new();
private RegexExplainerAgent _explainerAgent = new();

private void Window_Loaded(object sender, RoutedEventArgs e) {
    ucAi.AiControl.Configure(_generatorAgent);
    ucAi.AiControl.ImportData(LoadSavedSettings());
}

private void AgentCombo_SelectionChanged(object sender, SelectionChangedEventArgs e) {
    if (!IsLoaded) return;

    AIOptions newAgent = agentCombo.SelectedIndex == 0 ? _generatorAgent : _explainerAgent;
    ucAi.Configure(newAgent);  // Clears chat, keeps provider settings
}

You can also trigger agent switches programmatically and send a message:

private async void ExplainRegex_Click(object sender, RoutedEventArgs e) {
    agentCombo.SelectedIndex = 1;  // Switch to explainer agent
    await ucAi.SendMessage($"`{txtRegex.Text}`");  // Send the current pattern
}

Developer / Technical Notes

The control uses Micrsoft's Microsoft.SemanticKernel AI backend to talk with different AI model providers. Most providers can work with the standard OpenAI style protocol so can be added using the custom endpoint provider in our settings. Additional providers that work through Microsoft.SemanticKernel can be added pretty easily, look at AiModelProvider.cs for examples. GithubCopilotProvider.cs shows the most complex provider using a custom login flow and auto token discovery.

WinUI3 Beta Support

There is also a WinUi3 beta support version of this control. It largely works but is not as polished. There is not a good Markdown engine with regex highlighting for one for WinUI3 but we are using the older community toolkit which does give us limited highlight support. The WinUI3 version is not as well tested and may have other bugs.

About

a .NET WPF/WinUI Control to easily add an integrated AI assistant to any application

Resources

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published

Languages