Compare commits

...
59 Commits
Author SHA1 Message Date
Thorsten Sommer d0dbbccd56 Allow opening external links in an external browser (#127) 2024-09-07 18:36:57 +02:00
Thorsten Sommer 75576129fe Fixed external links (#126) 2024-09-07 16:26:04 +02:00
Thorsten Sommer 2efd08dbbc Prepared release v0.9.4 (#125) 2024-09-06 22:24:23 +02:00
Thorsten Sommer 4bd22108de Added synonym assistant (#124) 2024-09-06 22:02:20 +02:00
Thorsten Sommer 4ea767cb45 Revert MudBlazor to v7.6.0 (#123) 2024-09-06 13:14:51 +02:00
Thorsten Sommer 11846e279c Prepare release v0.9.3 (#121) 2024-09-05 20:53:51 +02:00
Thorsten Sommer c861d71383 Fix the send to chat scroll issue (#120) 2024-09-05 20:09:03 +02:00
Thorsten Sommer 6fbf32d8a2 Fix scrolling issue for long chats (#119) 2024-09-05 19:37:54 +02:00
Thorsten Sommer 4216a29325 Prepared release v0.9.2 (#117) 2024-09-05 11:26:05 +02:00
Thorsten Sommer 45770c2cd7 Fixed #115 (#116) 2024-09-05 11:13:40 +02:00
Thorsten Sommer 57905d5510 Fix the linux-arm build (#114) 2024-09-04 20:09:19 +02:00
Thorsten Sommer e999aab7c0 Prepared release v0.9.1 (#113) 2024-09-04 15:57:42 +02:00
Thorsten Sommer c47d3893ff Improve provider selection (#112) 2024-09-04 15:44:23 +02:00
Thorsten Sommer 09c1b77414 Cache the Rust build artifacts (#111) 2024-09-04 11:19:14 +02:00
Thorsten Sommer ea083fdec2 Bind mount the repo for linux-arm (#110) 2024-09-04 10:23:18 +02:00
Thorsten Sommer ceabcdb26e Prepared release v0.9.0 (#109) 2024-09-03 18:06:06 +02:00
Thorsten Sommer a25793ff11 Prepared release v0.9.0 (#108) 2024-09-03 18:03:13 +02:00
Thorsten Sommer 8d8a47b43b Update build instructions (#107) 2024-09-03 17:40:11 +02:00
Thorsten Sommer 2889581702 Changed release settings (#106) 2024-09-03 17:03:35 +02:00
Thorsten Sommer 12602ba8dd Improve initial log file (#105) 2024-09-03 16:13:57 +02:00
Thorsten Sommer e4b8487945 Fixed the handling of the signals field of the runtime API server (#104) 2024-09-01 21:54:23 +02:00
Thorsten Sommer fc64c0b505 Improve ipc (#102) 2024-09-01 20:10:03 +02:00
Thorsten Sommer 47f5756e5a Migrated to keyring v3.2.0 (#101) 2024-08-24 20:05:39 +02:00
Thorsten Sommer e4ef4bc06e Fixed an issue with the update file due to double quotes (#100) 2024-08-24 14:00:27 +02:00
Thorsten Sommer ae17104886 Prepared release v0.8.12 (#98) 2024-08-24 10:34:52 +02:00
Thorsten Sommer 2d82fcc5e1 Fix #65: changelog variable assignment (#97) 2024-08-24 10:24:00 +02:00
Thorsten Sommer 69a6fcf094 Moved legal check assistant to release 0.8.12 due to #65 (#96) 2024-08-24 09:31:06 +02:00
Thorsten Sommer 4aaa2fcbea Added a legal check assistant & the possibility to preselect some options (#94) 2024-08-23 21:47:07 +02:00
Thorsten Sommer d855a33e6f Fixed the changelog due to #65 (#95) 2024-08-23 21:43:42 +02:00
Thorsten Sommer ca16a749fd Prepare to release 0.8.12 (#93) 2024-08-23 18:18:56 +02:00
Thorsten Sommer bcdbf7dcbd Improved the rewrite assistant by letting you choose the voice for the sentence structure (#92) 2024-08-23 18:11:54 +02:00
Thorsten Sommer 537a608e7a Improved assistant footer handling (#91) 2024-08-23 13:44:27 +02:00
Thorsten Sommer a6c5b3de23 Improved the coding assistant by adding a button to delete a context (#90) 2024-08-23 11:23:36 +02:00
Thorsten Sommer 3fd0d4d5c1 Improved some default settings (#89) 2024-08-23 10:49:30 +02:00
Thorsten Sommer 5efb6b0158 Improved the chat options readability (#88) 2024-08-23 10:43:21 +02:00
Thorsten Sommer 1b3fff67ec Improved chat page by scrolling to the bottom after loading (#87) 2024-08-23 10:32:27 +02:00
Thorsten Sommer 3409872090 All assistants now have a button to copy their respective result to the clipboard (#86) 2024-08-23 08:59:30 +02:00
Thorsten Sommer 18718a6c47 Refactored the "send to" implementation of assistants (#85) 2024-08-22 21:27:16 +02:00
Thorsten Sommer 34884859d7 Add mail assistant (#84) 2024-08-22 15:41:35 +02:00
Thorsten Sommer 50bf0baff7 Prepared release v0.8.11 (#83) 2024-08-21 09:10:16 +02:00
Thorsten Sommer 83fe126c9f Fixed a bug where the reset function could lead to undesired behavior (#82) 2024-08-21 08:59:55 +02:00
Thorsten Sommer 37b38e6a40 Update readme (#81) 2024-08-21 08:45:29 +02:00
Thorsten Sommer 1d1af91f3e Refactor components (#80) 2024-08-21 08:30:01 +02:00
Thorsten Sommer a18cb22263 Fixed a bug where the chats in the workspace component were not sorted (#79) 2024-08-19 20:19:55 +02:00
Thorsten Sommer f8e06faea5 Send to chat (#77) 2024-08-18 21:48:35 +02:00
Thorsten Sommer 8aeeda5dca Prepared release v0.8.9 (#76) 2024-08-18 12:41:41 +02:00
Thorsten Sommer 909fbe89bb Assistant send result (#75) 2024-08-18 12:32:18 +02:00
Thorsten Sommer ea0ead58f8 Prepare release v0.8.8 (#74) 2024-08-14 08:39:32 +02:00
Thorsten Sommer 3e59f23193 Added a rewrite and text improvement assistant (#73) 2024-08-13 20:53:09 +02:00
Thorsten Sommer 05a7e44de4 Add a grammar and spell checker assistant (#72) 2024-08-13 08:57:58 +02:00
Thorsten Sommer 77e7e044e3 Upgraded MudBlazor to v7.6.0 (#71) 2024-08-11 20:59:05 +02:00
Thorsten Sommer 6b7800b615 Add agenda assistant (#69) 2024-08-05 21:12:52 +02:00
Thorsten Sommer 1ec86ec1a6 Restructuring of the assistant overview: the assistants are now displayed in different categories (#68) 2024-08-02 13:50:29 +02:00
Thorsten Sommer e08687a5ca Upgraded MudBlazor (#67) 2024-08-02 07:55:37 +02:00
Thorsten Sommer ab0b855769 Removed code formatting due to #65 (#66) 2024-08-02 07:17:48 +02:00
Thorsten Sommer 4cb49d9f27 Allow reading web content into assistants (#63) 2024-08-01 21:53:28 +02:00
Thorsten Sommer 2cea53d2a9 Improved assistant overview (#61) 2024-07-29 09:15:23 +02:00
Thorsten Sommer 155c2d72f8 Added possibility to configure a default provider for chats (#60) 2024-07-29 09:06:01 +02:00
Thorsten Sommer 60febfec86 Improve settings.json, fixes #53 (#59) 2024-07-28 21:18:17 +02:00
257 changed files with 8194 additions and 2587 deletions

No files matched your search

+23 -5
View File
@@ -489,7 +489,7 @@ jobs:
uses: actions/cache@v4
id: linux_arm_cache
with:
path: $RUNNER_TEMP/linux_arm_qemu_cache.img
path: ${{ runner.temp }}/linux_arm_qemu_cache.img
# When the entire key matches, Rust might just create the bundles using the current .NET build:
key: target-linux-arm64-rust-${{ env.RUST_VERSION }}-dependencies-${{ env.CARGO_LOCK_HASH }}
@@ -544,7 +544,7 @@ jobs:
- name: Add the built runner image to the cache
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
run: |
mv ${{ steps.build-linux-arm-runner.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img
mv ${{ steps.build-linux-arm-runner.outputs.image }} ${{ runner.temp }}/linux_arm_qemu_cache.img
- name: Build Tauri project
if: ${{ env.SKIP != 'true' }}
@@ -552,11 +552,12 @@ jobs:
id: build-linux-arm
with:
base_image: file://$RUNNER_TEMP/linux_arm_qemu_cache.img
base_image: file://${{ runner.temp }}/linux_arm_qemu_cache.img
cpu: cortex-a53
optimize_image: false
copy_artifact_path: runtime
copy_artifact_dest: result
bind_mount_repository: true
#
# We do not need to set the PRIVATE_PUBLISH_KEY and PRIVATE_PUBLISH_KEY_PASSWORD here,
@@ -573,7 +574,17 @@ jobs:
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
source "$HOME/.cargo/env"
cd runtime
# Try to restore the Rust cache from previous build:
mkdir -p /rust-cache/target
rm -fr target
cp -Rp /rust-cache/target target
cargo tauri build --target aarch64-unknown-linux-gnu --bundles deb
# Save the built libraries for the next job:
rm -fr /rust-cache/target
cp -Rp target /rust-cache
- name: Debug
if: ${{ env.SKIP != 'true' }}
@@ -729,6 +740,9 @@ jobs:
# Replace newlines in changelog with \n
changelog=$(echo "$CHANGELOG" | awk '{printf "%s\\n", $0}')
# Escape double quotes in changelog:
changelog=$(echo "$changelog" | sed 's/"/\\"/g')
# Create the latest.json file:
cat <<EOOOF > $GITHUB_WORKSPACE/release/assets/latest.json
{
@@ -794,7 +808,11 @@ jobs:
- name: Append scan results to changelog
run: |
changelog="${{ env.CHANGELOG }}"
changelog=$(cat <<'EOOOOOOOF'
${{ env.CHANGELOG }}
EOOOOOOOF
)
links="${{ steps.virus_total.outputs.analysis }}"
# Create a temporary file for the changelog:
@@ -838,7 +856,7 @@ jobs:
- name: Create release
uses: softprops/action-gh-release@v2
with:
prerelease: false
prerelease: true
draft: false
make_latest: true
body: ${{ env.CHANGELOG }}
+2 -1
View File
@@ -1,5 +1,6 @@
# MindWork AI Studio
![MindWork AI Studio](documentation/AI%20Studio.png)
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png)
![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png)
MindWork AI Studio is a desktop application available for macOS, Windows, and Linux. It provides a unified chat interface for Large Language Models (LLMs). You bring your own API key for the respective LLM provider to use the models. The API keys are securely stored by the operating system.
+108
View File
@@ -0,0 +1,108 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
// ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Agents;
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : IAgent
{
protected SettingsManager SettingsManager { get; init; } = settingsManager;
protected ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary>
/// Represents the type or category of this agent.
/// </summary>
protected abstract Type Type { get; }
/// <summary>
/// The name of the agent.
/// </summary>
public abstract string Id { get; }
/// <summary>
/// The agent's job description. Will be used for the system prompt.
/// </summary>
protected abstract string JobDescription { get; }
/// <summary>
/// Represents the system prompt provided for the agent.
/// </summary>
protected abstract string SystemPrompt(string additionalData);
#region Implementation of IAgent
public abstract AIStudio.Settings.Provider? ProviderSettings { get; set; }
public abstract Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData);
public abstract Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData);
public abstract Task<bool> MadeDecision(ContentBlock input);
public abstract IReadOnlyCollection<ContentBlock> GetContext();
public abstract IReadOnlyCollection<ContentBlock> GetAnswers();
#endregion
protected ChatThread CreateChatThread(string systemPrompt) => new()
{
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
SystemPrompt = systemPrompt,
Blocks = [],
};
protected DateTimeOffset AddUserRequest(ChatThread thread, string request)
{
var time = DateTimeOffset.Now;
thread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = request,
},
});
return time;
}
protected async Task AddAIResponseAsync(ChatThread thread, DateTimeOffset time)
{
if(this.ProviderSettings is null)
return;
var providerSettings = this.ProviderSettings.Value;
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
var resultingContentBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
};
thread.Blocks.Add(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(providerSettings.CreateProvider(this.Logger), this.SettingsManager, providerSettings.Model, thread);
}
}
@@ -0,0 +1,93 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, rng)
{
private static readonly ContentBlock EMPTY_BLOCK = new()
{
Content = null,
ContentType = ContentType.NONE,
Role = ChatRole.AGENT,
Time = DateTimeOffset.UtcNow,
};
private readonly List<ContentBlock> context = new();
private readonly List<ContentBlock> answers = new();
#region Overrides of AgentBase
public override Settings.Provider? ProviderSettings { get; set; }
protected override Type Type => Type.SYSTEM;
public override string Id => "Text Content Cleaner";
protected override string JobDescription =>
"""
You receive a Markdown document as input. Your goal is to identify the main content of the document
and return it including Markdown formatting. Remove areas that do not belong to the main part of the
document. For a blog article, return only the text of the article with its formatting. For a scientific
paper, only the contents of the paper. Delete elements of navigation, advertisements, HTML artifacts,
cookie banners, etc. If the content contains images, these images remain. The same applies to links.
Ensure that links and images are present as valid Markdown:
- Syntax of links: [link text](URL)
- Syntax of images: ![alt text](URL)
If you find relative links or images with relative paths, correct them to absolute paths. For this
purpose, here is the source URL:
""";
protected override string SystemPrompt(string additionalData) => $"{this.JobDescription} `{additionalData}`.";
/// <inheritdoc />
public override async Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData)
{
// We process the last block of the chat thread. Then, we add the result
// to the chat thread as the last block:
var answer = await this.ProcessInput(chatThread.Blocks[^1], additionalData);
chatThread.Blocks.Add(answer);
this.context.Clear();
this.context.AddRange(chatThread.Blocks);
return chatThread;
}
// <inheritdoc />
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
{
if (input.Content is not ContentText text)
return EMPTY_BLOCK;
if(text.InitialRemoteWait || text.IsStreaming)
return EMPTY_BLOCK;
if(string.IsNullOrWhiteSpace(text.Text))
return EMPTY_BLOCK;
if(!additionalData.TryGetValue("sourceURL", out var sourceURL) || string.IsNullOrWhiteSpace(sourceURL))
return EMPTY_BLOCK;
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
var time = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, time);
var answer = thread.Blocks[^1];
this.answers.Add(answer);
return answer;
}
// <inheritdoc />
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
// <inheritdoc />
public override IReadOnlyCollection<ContentBlock> GetContext() => this.context;
// <inheritdoc />
public override IReadOnlyCollection<ContentBlock> GetAnswers() => this.answers;
#endregion
}
+55
View File
@@ -0,0 +1,55 @@
using AIStudio.Chat;
namespace AIStudio.Agents;
public interface IAgent
{
/// <summary>
/// Gets the name of the agent.
/// </summary>
public string Id { get; }
/// <summary>
/// The provider to use for this agent.
/// </summary>
public AIStudio.Settings.Provider? ProviderSettings { get; set; }
/// <summary>
/// Processes a chat thread (i.e., context) and returns the updated thread.
/// </summary>
/// <param name="chatThread">The chat thread to process. The thread is the context for the agent.</param>
/// <param name="additionalData">Additional data to use for processing the chat thread.</param>
/// <returns>The updated chat thread. The last content block of the thread is the agent's response.</returns>
public Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData);
/// <summary>
/// Processes the input content block and returns the agent's response.
/// </summary>
/// <param name="input">The content block to process. It represents the input.</param>
/// <param name="additionalData">Additional data to use for processing the input.</param>
/// <returns>The content block representing the agent's response.</returns>
public Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData);
/// <summary>
/// The agent makes a decision based on the input.
/// </summary>
/// <param name="input">The content block to process. Should be a question or a request.</param>
/// <returns>
/// True if a decision has been made based on the input, false otherwise.
/// </returns>
public Task<bool> MadeDecision(ContentBlock input);
/// <summary>
/// Retrieves the context of the agent.
/// </summary>
/// <returns>The collection of content blocks representing the agent's context. This includes the user's and the other agent's messages.</returns>
public IReadOnlyCollection<ContentBlock> GetContext();
/// <summary>
/// Retrieves the answers from the agent's context.
/// </summary>
/// <returns>
/// The collection of content blocks representing the answers provided by this agent.
/// </returns>
public IReadOnlyCollection<ContentBlock> GetAnswers();
}
+27
View File
@@ -0,0 +1,27 @@
namespace AIStudio.Agents;
public enum Type
{
/// <summary>
/// Represents an unspecified agent type.
/// </summary>
UNSPECIFIED = 0,
/// <summary>
/// Represents a conversational agent who produces human-like responses and feedback (depending on the context and its job description).
/// For example, an expert agent for a specific domain. Answers might be detailed and informative.
/// </summary>
CONVERSATIONAL,
/// <summary>
/// Represents a worker agent type who performs tasks and provides information or services (depending on the context and its job description).
/// For example, a quality assurance agent who assesses the quality of a product or service. Answers might be short and concise.
/// </summary>
WORKER,
/// <summary>
/// Represents the system agent type who processes the input and provides a specific response (depending on the context and its job description).
/// For example, a HTML content agent who processes the arbitrary HTML content and provides a structured Markdown response. Answers might be structured and formatted.
/// </summary>
SYSTEM,
}
+30
View File
@@ -0,0 +1,30 @@
@using Microsoft.AspNetCore.Components.Web
@* ReSharper disable Html.PathError *@
<!DOCTYPE html>
<html lang="en">
<head>
<title>MindWork AI Studio</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<link href="app.css" rel="stylesheet" />
<HeadOutlet/>
<script src="diff.js"></script>
</head>
<body style="overflow: hidden;">
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)"/>
<script src="_framework/blazor.web.js" autostart="false"></script>
<script src="boot.js"></script>
<script src="system/MudBlazor/MudBlazor.min.js"></script>
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
<script src="app.js"></script>
</body>
</html>
@@ -0,0 +1,55 @@
@attribute [Route(Routes.ASSISTANT_AGENDA)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputContent" Validation="@this.ValidateContent" Label="Content list" Variant="Variant.Outlined" Lines="6" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Bullet list the content of the meeting, seminar, etc. roughly. Use dashes (-) to separate the items." Placeholder="@PLACEHOLDER_CONTENT" Class="mb-3" Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt"/>
<MudSelect T="string" Label="(Optional) What topics should be the focus?" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
@foreach (var contentLine in this.contentLines)
{
@if(!this.justBriefly.Contains(contentLine))
{
<MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
}
}
</MudSelect>
<MudSelect T="string" Label="(Optional) What topics should only be briefly addressed?" MultiSelection="@true" @bind-SelectedValues="@this.justBriefly" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
@foreach (var contentLine in this.contentLines)
{
@if(!this.selectedFoci.Contains(contentLine))
{
<MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
}
}
</MudSelect>
<MudTextField T="string" @bind-Text="@this.inputObjective" Validation="@this.ValidateObjective" Label="Objective" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Lines="3" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the objective(s) of the meeting, seminar, etc. What should be achieved?" Placeholder="Discuss the current project status and plan the next steps." Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputModerator" Validation="@this.ValidateModerator" Label="Moderator" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person3" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Who will moderate the meeting, seminar, etc.?" Placeholder="Jane Doe" Class="mb-3" />
<MudTextField T="string" @bind-Text="@this.inputDuration" Validation="@this.ValidateDuration" Label="Duration" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="How long will the meeting, seminar, etc. last? E.g., '2 hours', or '2 days (first day 8 hours, then 4 hours)', etc." Placeholder="2 hours" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputStartTime" Validation="@this.ValidateStartTime" Label="Start time" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="When will the meeting, seminar, etc. start? E.g., '9:00 AM', or '9:00 AM (CET)', etc. When the meeting is a multi-day event, specify the start time for each day." Placeholder="9:00 AM" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputWhoIsPresenting" Label="(Optional) Who is presenting?" Variant="Variant.Outlined" Lines="6" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="(Optional) List the persons who will present at the meeting, seminar, etc. Use dashes (-) to separate the items." Placeholder="@PLACEHOLDER_WHO_IS_PRESENTING" Class="mb-3"/>
<MudTextSwitch Label="Do the participants need to get to know each other first?" @bind-Value="@this.introduceParticipants" LabelOn="Yes, introduce participants" LabelOff="No, participants know each other" />
<EnumSelection T="NumberParticipants" @bind-Value="@this.numberParticipants" NameFunc="@(participants => participants.Name())" Icon="@Icons.Material.Filled.Group" Label="Number of participants" ValidateSelection="@this.ValidateNumberParticipants"/>
<MudTextSwitch Label="Should the participants be involved passively or actively?" @bind-Value="@this.activeParticipation" LabelOn="Active participation, like world café, discussions, etc." LabelOff="Passive participation, like presentations, lectures, etc." />
<MudTextSwitch Label="Is this a virtual event, e.g., a call or webinar?" @bind-Value="@this.isMeetingVirtual" LabelOn="Yes, this is a virtual event" LabelOff="No, this is a physical event" />
@if (!this.isMeetingVirtual)
{
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudTextField T="string" @bind-Text="@this.inputLocation" Validation="@this.ValidateLocation" Label="Location" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.MyLocation" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Where will the meeting, seminar, etc. take place?" Placeholder="Hamburg, Germany" Class="mb-3"/>
<MudTextSwitch Label="Should there be a joint dinner?" @bind-Value="@this.goingToDinner" LabelOn="Yes, there should be a joint dinner" LabelOff="No, there should be no joint dinner" />
<MudTextSwitch Label="Should there be a social activity?" @bind-Value="@this.doingSocialActivity" LabelOn="Yes, there should be a social activity" LabelOff="No, there should be no social activity" />
<MudTextSwitch Label="Do participants need to arrive and depart?" @bind-Value="@this.needToArriveAndDepart" LabelOn="Yes, participants need to arrive and depart" LabelOff="No, participants do not need to arrive and depart" />
<MudStack Row="@true" Wrap="Wrap.Wrap">
<MudTextSlider T="int" Label="Approx. duration of the lunch break" @bind-Value="@this.durationLunchBreak" Min="30" Max="120" Step="5" Unit="minutes"/>
<MudTextSlider T="int" Label="Approx. duration of the coffee or tea breaks" @bind-Value="@this.durationBreaks" Min="10" Max="60" Step="5" Unit="minutes"/>
</MudStack>
</MudPaper>
}
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.CreateAgenda()">
Create agenda
</MudButton>
@@ -0,0 +1,503 @@
using System.Text;
using AIStudio.Chat;
namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
protected override string Title => "Agenda Planner";
protected override string Description =>
"""
This agenda planner helps you create a structured agenda for your meeting or seminar. Just provide some basic
information about the event, and the assistant will generate an agenda for you. You can also specify the
duration, the start time, the location, the target language, and other details.
""";
protected override string SystemPrompt =>
$"""
You are a friendly assistant who supports the planning and consultation of events. You receive
metadata about an event and create an agenda from it. Additionally, you provide advice on what
the organizer and moderator should consider. When creating the agenda, you take into account that
time for questions and discussions needs to be scheduled. For large events, you take into account
the logistical challenges that come with an increasing number of participants.
If the user is planning interactions with participants, you know various methods. For example:
- **Townhall Meeting**: An open assembly where participants can ask questions and provide feedback directly to leaders or speakers. Good moderation is necessary to queue contributions and hand the floor to the respective person. Additionally, one person should take notes visible to all. Ideally, this should not be the moderator.
- **Speed Dating**: Short, structured interaction rounds where participants get the chance to quickly introduce themselves and make initial connections. A long row of tables is needed so that two people can sit opposite each other. A moderator with a stopwatch must ensure that participants rotate and change seats every 3 to 5 minutes.
- **World Café**: Participants discuss in small groups at tables and rotate after set intervals to exchange thoughts and develop ideas further. Each table needs a host. Notes should be taken on a flipchart or digitally at each table. At the end, the table hosts could present their insights to all participants in a plenary session.
- **Fishbowl**: A group discusses in an inner circle while a larger group listens from the outside. Occasionally, listeners can move into the inner circle to actively participate.
- **BarCamp (Unconference)**: Participants contribute to part of the agenda themselves and can act as both presenters and listeners. Anyone can propose and lead a session. However, there should be initial moderation to guide this meta-process.
- **Open Space Technology**: A method for large groups where participants bring up their own topics and then work intensively in small groups. At the end, the groups can present their findings to all participants in a plenary session.
- **Panel Discussion**: A group of experts discusses a specific topic in front of an audience, followed by a question-and-answer session.
- **Breakout Sessions**: Larger groups are divided into smaller, topic-related groups to discuss specific issues more deeply and interactively.
- **Interactive Polling**: Real-time surveys during the meeting or seminar using digital technology to collect immediate feedback from participants. With an increasing number of participants, technical requirements also become more complex: not every Wi-Fi router can handle several hundred or thousand devices. Additionally, the moderator or speaker must have a stable network connection to display the results. It would be problematic if the speaker depends on the same Wi-Fi as the participants: if the Wi-Fi crashes due to the high number of participants, the speaker cannot show the results. Therefore, a LAN for the speaker or the use of a mobile hotspot would be a better choice.
- **Peer Learning Groups**: Small groups of participants work together to learn from each other and collaboratively develop solutions for specific challenges.
- **Role Play/Simulation**: Participants take on roles to simulate certain scenarios, gaining practical experience and promoting dynamic discussions.
- **Round Table Discussions**: Small group discussions on specific topics, often without formal moderation, to encourage open and equal dialogue.
- **Mind Mapping**: A method for visualizing ideas and concepts, which can foster creative exchange within groups.
- **Case Study Analysis**: Groups analyze real or hypothetical case studies and present their solutions or insights.
- **Silent Meeting/Writing**: Participants write down their thoughts and questions, which are then collected and discussed to balance out loud or dominant voices.
- **Debate**: Participants argue for or against a specific topic, often with a moderator who ensures that the discussion remains constructive.
- **Workshops**: Interactive sessions where participants work together on specific tasks or challenges.
- **Brainstorming**: A method for generating ideas in a group setting, often with a focus on quantity and creativity.
- **Design Thinking**: A structured approach to innovation solutions. Design thinking is structured in six phases: understand, observe, point of view, ideate, prototype, and test. Depending on the number of participants, there are two moderates needed. One moderator leads the process, the other supports the participants in their work. Design thinking may takes up to two days.
You output the agenda in the following Markdown format:
# [Agenda Title]
- 09:00 - 09:05 (5 minutes): Welcome
- 09:05 - 09:15 (10 minutes): Introduction
- ...
{this.SystemPromptSocialActivity()}
{this.SystemPromptDinner()}
# Organizational notes
[Your advice for the organizer goes here, like room setup, technical requirements, etc.]
# Moderation notes
[Your advice for the moderator goes here]
Output the agenda in the following language: {this.PromptLanguage()}.
""";
private const string PLACEHOLDER_CONTENT = """
- Project status
- We need to discuss how to proceed
- Problem solving in work package 3 possible?
- Concerns of the members
""";
private const string PLACEHOLDER_WHO_IS_PRESENTING = """
- John Doe: Project status
- Mary Jane: Work package 3
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputContent = string.Empty;
this.contentLines.Clear();
this.selectedFoci = [];
this.justBriefly = [];
this.inputWhoIsPresenting = string.Empty;
if (!this.MightPreselectValues())
{
this.inputTopic = string.Empty;
this.inputName = string.Empty;
this.inputDuration = string.Empty;
this.inputStartTime = string.Empty;
this.inputObjective = string.Empty;
this.inputModerator = string.Empty;
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.introduceParticipants = false;
this.isMeetingVirtual = true;
this.inputLocation = string.Empty;
this.goingToDinner = false;
this.doingSocialActivity = false;
this.needToArriveAndDepart = false;
this.durationLunchBreak = 0;
this.durationBreaks = 0;
this.activeParticipation = false;
this.numberParticipants = NumberParticipants.NOT_SPECIFIED;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)
{
this.inputTopic = this.SettingsManager.ConfigurationData.Agenda.PreselectTopic;
this.inputName = this.SettingsManager.ConfigurationData.Agenda.PreselectName;
this.inputDuration = this.SettingsManager.ConfigurationData.Agenda.PreselectDuration;
this.inputStartTime = this.SettingsManager.ConfigurationData.Agenda.PreselectStartTime;
this.inputObjective = this.SettingsManager.ConfigurationData.Agenda.PreselectObjective;
this.inputModerator = this.SettingsManager.ConfigurationData.Agenda.PreselectModerator;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage;
this.introduceParticipants = this.SettingsManager.ConfigurationData.Agenda.PreselectIntroduceParticipants;
this.isMeetingVirtual = this.SettingsManager.ConfigurationData.Agenda.PreselectIsMeetingVirtual;
this.inputLocation = this.SettingsManager.ConfigurationData.Agenda.PreselectLocation;
this.goingToDinner = this.SettingsManager.ConfigurationData.Agenda.PreselectJointDinner;
this.doingSocialActivity = this.SettingsManager.ConfigurationData.Agenda.PreselectSocialActivity;
this.needToArriveAndDepart = this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart;
this.durationLunchBreak = this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime;
this.durationBreaks = this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime;
this.activeParticipation = this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation;
this.numberParticipants = this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants;
return true;
}
return false;
}
private string inputTopic = string.Empty;
private string inputName = string.Empty;
private string inputContent = string.Empty;
private string inputDuration = string.Empty;
private string inputStartTime = string.Empty;
private IEnumerable<string> selectedFoci = new HashSet<string>();
private IEnumerable<string> justBriefly = new HashSet<string>();
private string inputObjective = string.Empty;
private string inputModerator = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private bool introduceParticipants;
private bool isMeetingVirtual = true;
private string inputLocation = string.Empty;
private bool goingToDinner;
private bool doingSocialActivity;
private bool needToArriveAndDepart;
private int durationLunchBreak;
private int durationBreaks;
private bool activeParticipation;
private NumberParticipants numberParticipants;
private string inputWhoIsPresenting = string.Empty;
private readonly List<string> contentLines = [];
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_AGENDA_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContent = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private void OnContentChanged(string content)
{
var previousSelectedFoci = new HashSet<string>();
var previousJustBriefly = new HashSet<string>();
this.contentLines.Clear();
foreach (var line in content.AsSpan().EnumerateLines())
{
var trimmedLine = line.Trim();
if (trimmedLine.StartsWith("-"))
trimmedLine = trimmedLine[1..].Trim();
if (trimmedLine.Length == 0)
continue;
var finalLine = trimmedLine.ToString();
if(this.selectedFoci.Any(x => x.StartsWith(finalLine, StringComparison.InvariantCultureIgnoreCase)))
previousSelectedFoci.Add(finalLine);
if(this.justBriefly.Any(x => x.StartsWith(finalLine, StringComparison.InvariantCultureIgnoreCase)))
previousJustBriefly.Add(finalLine);
this.contentLines.Add(finalLine);
}
this.selectedFoci = previousSelectedFoci;
this.justBriefly = previousJustBriefly;
}
private string? ValidateLocation(string location)
{
if(!this.isMeetingVirtual && string.IsNullOrWhiteSpace(location))
return "Please provide a location for the meeting or the seminar.";
return null;
}
private string? ValidateNumberParticipants(NumberParticipants selectedSize)
{
if(selectedSize is NumberParticipants.NOT_SPECIFIED)
return "Please select the number of participants.";
return null;
}
private string? ValidateTargetLanguage(CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Please select a target language for the agenda.";
return null;
}
private string? ValidateDuration(string duration)
{
if(string.IsNullOrWhiteSpace(duration))
return "Please provide a duration for the meeting or the seminar, e.g. '2 hours', or '2 days (8 hours and 4 hours)', etc.";
return null;
}
private string? ValidateStartTime(string startTime)
{
if(string.IsNullOrWhiteSpace(startTime))
return "Please provide a start time for the meeting or the seminar. When the meeting is a multi-day event, specify the start time for each day, e.g. '9:00 AM, 10:00 AM', etc.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string? ValidateTopic(string topic)
{
if(string.IsNullOrWhiteSpace(topic))
return "Please provide a topic for the agenda. What is the meeting or the seminar about?";
return null;
}
private string? ValidateName(string name)
{
if(string.IsNullOrWhiteSpace(name))
return "Please provide a name for the meeting or the seminar.";
return null;
}
private string? ValidateContent(string content)
{
if(string.IsNullOrWhiteSpace(content))
return "Please provide some content for the agenda. What are the main points of the meeting or the seminar?";
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
if(!line.TrimStart().StartsWith('-'))
return "Please start each line of your content list with a dash (-) to create a bullet point list.";
return null;
}
private string? ValidateObjective(string objective)
{
if(string.IsNullOrWhiteSpace(objective))
return "Please provide an objective for the meeting or the seminar. What do you want to achieve?";
return null;
}
private string? ValidateModerator(string moderator)
{
if(string.IsNullOrWhiteSpace(moderator))
return "Please provide a moderator for the meeting or the seminar. Who will lead the discussion?";
return null;
}
private async Task CreateAgenda()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
Create an agenda for the meeting or seminar wit the title '{this.inputName}' using the following details:
- The overall topic is '{this.inputTopic}'.
- The objective of the meeting is '{this.inputObjective}'.
- The moderator is '{this.inputModerator}'.
- The planned duration is '{this.inputDuration}'.
- The planned start time is '{this.inputStartTime}'.
{this.PromptGettingToKnowEachOther()}
- The number of participants is '{this.numberParticipants.Name()}'.
{this.PromptActiveParticipation()}
{this.PromptVirtualMeeting()}
{this.PromptPhysicalMeeting()}
Here is draft what was planned for the meeting:
{this.inputContent}
Please do not follow this draft strictly. It is just a suggestion.
{this.PromptFoci()}
{this.PromptBriefly()}
{this.PromptWhoIsPresenting()}
""");
await this.AddAIResponseAsync(time);
}
private string PromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.AS_IS)
return "Use the same language as the input.";
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private string PromptPhysicalMeeting()
{
if(this.isMeetingVirtual)
return string.Empty;
return $"""
- The meeting takes place at the following location: '{this.inputLocation}'.
{this.PromptJoiningDinner()}
{this.PromptSocialActivity()}
{this.PromptArriveAndDepart()}
- In case a lunch break is necessary, the break should about {this.durationLunchBreak} minutes long.
- In case any breaks are necessary, the breaks should be about {this.durationBreaks} minutes long.
""";
}
private string PromptArriveAndDepart()
{
if (this.needToArriveAndDepart)
return "- The participants should have time to arrive and depart.";
return "- The participants do not need to arrive and depart.";
}
private string PromptSocialActivity()
{
if (this.doingSocialActivity)
return "- The participants will engage in a social activity after the meeting or seminar. This can be a team-building exercise, a sightseeing tour, or a visit to a local attraction. Please make a recommendation for an activity at '{this.inputLocation}'.";
return "- The participants will not engage in a social activity after the meeting or seminar.";
}
private string SystemPromptSocialActivity()
{
if(this.doingSocialActivity)
return """
- 16:00 Departure to the social event [Consider the time needed to get to the location].
- 17:00 [Insert your advice for the social activity here].
""";
return string.Empty;
}
private string SystemPromptDinner()
{
if(this.goingToDinner)
return """
- 19:00 Departure to the restaurant [Consider the time needed to get to the restaurant].
- 20:00 [Insert your advice for the dinner here.].
""";
return string.Empty;
}
private string PromptJoiningDinner()
{
if (this.goingToDinner)
return $"- The participants will join for a dinner after the meeting or seminar. Please make a recommendation for a restaurant at '{this.inputLocation}'.";
return "- The participants will not join for a dinner after the meeting or seminar.";
}
private string PromptVirtualMeeting()
{
if (this.isMeetingVirtual)
return "- The meeting or seminar will be held virtually, e.g, a call or webinar. The participants will join online.";
return "- The meeting or seminar will be held in person.";
}
private string PromptActiveParticipation()
{
if (this.activeParticipation)
return "- The participants should actively participate in the meeting or seminar. This can be done through discussions, questions, and contributions to the content.";
return "- The participants do not need to actively participate.";
}
private string PromptGettingToKnowEachOther()
{
if (this.introduceParticipants)
return "- The participants should introduce themselves to get to know each other. This can be done in a round or in pairs. The moderator should ensure that everyone has the opportunity to speak.";
return "- The participants do not need to introduce themselves.";
}
private string PromptFoci()
{
if (this.selectedFoci.Any())
{
return $"""
Out of this content, the following points should be focused on:
{this.ConvertItemsToMarkdown(this.selectedFoci)}
""";
}
return string.Empty;
}
private string PromptBriefly()
{
if (this.justBriefly.Any())
{
return $"""
The following points should be just briefly mentioned:
{this.ConvertItemsToMarkdown(this.justBriefly)}
""";
}
return string.Empty;
}
private string PromptWhoIsPresenting()
{
if (!string.IsNullOrWhiteSpace(this.inputWhoIsPresenting))
{
return $"""
The following persons will present the content:
{this.inputWhoIsPresenting}
""";
}
return string.Empty;
}
private string ConvertItemsToMarkdown(IEnumerable<string> items)
{
var markdown = new StringBuilder();
foreach (var item in items)
markdown.AppendLine($"- {item}");
return markdown.ToString();
}
}
@@ -0,0 +1,18 @@
namespace AIStudio.Assistants.Agenda;
public enum NumberParticipants
{
NOT_SPECIFIED,
PEER_TO_PEER, // 2 participants
SMALL_GROUP, // 3 - 5 participants
LARGE_GROUP, // 6 - 12 participants
MULTIPLE_SMALL_GROUPS, // 13 - 20 participants
MULTIPLE_LARGE_GROUPS, // 21 - 30 participants
SYMPOSIUM, // 31 - 100 participants
CONFERENCE, // 101 - 200 participants
CONGRESS, // 201 - 1000 participants
LARGE_EVENT, // 1000+ participants
}
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.Agenda;
public static class NumberParticipantsExtensions
{
public static string Name(this NumberParticipants numberParticipants) => numberParticipants switch
{
NumberParticipants.NOT_SPECIFIED => "Please select how many participants are expected",
NumberParticipants.PEER_TO_PEER => "2 (peer to peer)",
NumberParticipants.SMALL_GROUP => "3 - 5 (small group)",
NumberParticipants.LARGE_GROUP => "6 - 12 (large group)",
NumberParticipants.MULTIPLE_SMALL_GROUPS => "13 - 20 (multiple small groups)",
NumberParticipants.MULTIPLE_LARGE_GROUPS => "21 - 30 (multiple large groups)",
NumberParticipants.SYMPOSIUM => "31 - 100 (symposium)",
NumberParticipants.CONFERENCE => "101 - 200 (conference)",
NumberParticipants.CONGRESS => "201 - 1,000 (congress)",
NumberParticipants.LARGE_EVENT => "1,000+ (large event)",
_ => "Unknown"
};
}
@@ -0,0 +1,98 @@
@using AIStudio.Chat
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
<InnerScrolling HeaderHeight="6em">
<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>
<Issues IssuesData="@(this.inputIssues)"/>
@if (this.ShowDedicatedProgress && this.isProcessing)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
@if (this.ShowResult && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
</ChildContent>
<FooterContent>
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
@foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
{
if (assistant is Components.NONE || this.Component == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
@foreach (var button in this.FooterButtons)
{
switch (button)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
case SendToButton sendToButton:
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Color="Color.Info">
@foreach (var assistant in Enum.GetValues<Components>().OrderBy(n => n.Name().Length))
{
if(assistant is Components.NONE || sendToButton.Self == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
</MudStack>
</FooterContent>
</InnerScrolling>
@@ -0,0 +1,235 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase
{
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
protected ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
[Inject]
protected RustService RustService { get; init; } = null!;
[Inject]
protected NavigationManager NavigationManager { get; init; } = null!;
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
internal const string RESULT_DIV_ID = "assistantResult";
protected abstract string Title { get; }
protected abstract string Description { get; }
protected abstract string SystemPrompt { get; }
protected abstract Tools.Components Component { get; }
protected virtual Func<string> Result2Copy => () => this.resultingContentBlock is null ? string.Empty : this.resultingContentBlock.Content switch
{
ContentText textBlock => textBlock.Text,
_ => string.Empty,
};
protected abstract void ResetFrom();
protected abstract bool MightPreselectValues();
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual bool ShowDedicatedProgress => false;
protected virtual ChatThread ConvertToChatThread => this.chatThread ?? new();
protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form;
protected bool inputIsValid;
protected ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
private bool isProcessing;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnParametersSetAsync();
}
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 string? ValidatingProvider(AIStudio.Settings.Provider provider)
{
if(provider.UsedProvider == Providers.NONE)
return "Please select a provider.";
return null;
}
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<string> 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);
this.isProcessing = true;
this.StateHasChanged();
// 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.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
// Return the AI response:
return aiText.Text;
}
protected async Task CopyToClipboard()
{
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
}
private static string? GetButtonIcon(string icon)
{
if(string.IsNullOrWhiteSpace(icon))
return null;
return icon;
}
private Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{
var contentToSend = sendToButton.UseResultingContentBlockData switch
{
false => sendToButton.GetText(),
true => this.resultingContentBlock?.Content switch
{
ContentText textBlock => textBlock.Text,
_ => string.Empty,
},
};
var sendToData = destination.GetData();
switch (destination)
{
case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
break;
default:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, contentToSend);
break;
}
this.NavigationManager.NavigateTo(sendToData.Route);
return Task.CompletedTask;
}
private async Task InnerResetForm()
{
this.resultingContentBlock = null;
this.providerSettings = default;
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false;
this.inputIssues = [];
this.form?.ResetValidation();
this.StateHasChanged();
this.form?.ResetValidation();
}
}
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace AIStudio.Components;
namespace AIStudio.Assistants;
//
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
//
public abstract class AssistantBaseCore : AssistantBase
@@ -1,12 +1,12 @@
@page "/assistant/coding"
@using AIStudio.Settings
@attribute [Route(Routes.ASSISTANT_CODING)]
@inherits AssistantBaseCore
<MudExpansionPanels Class="mb-3">
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
{
var codingContext = this.codingContexts[contextIndex];
<ExpansionPanel HeaderText="@codingContext.Id" HeaderIcon="@Icons.Material.Filled.Code">
var index = contextIndex;
<ExpansionPanel HeaderText="@codingContext.Id" HeaderIcon="@Icons.Material.Filled.Code" ShowEndButton="@true" EndButtonColor="Color.Error" EndButtonIcon="@Icons.Material.Filled.Delete" EndButtonTooltip="Delete context" EndButtonClickAsync="@(() => this.DeleteContext(index))">
<CodingContextItem @bind-CodingContext="@codingContext"/>
</ExpansionPanel>
}
@@ -16,9 +16,7 @@
</MudButton>
<MudStack Row="@false" Class="mb-3">
<MudSwitch T="bool" @bind-Value="@this.provideCompilerMessages" Color="Color.Primary">
@(this.provideCompilerMessages ? "Provide compiler messages" : "Provide no compiler messages")
</MudSwitch>
<MudTextSwitch Label="Do you want to provide compiler messages?" @bind-Value="@this.provideCompilerMessages" LabelOn="Yes, provide compiler messages" LabelOff="No, there are no compiler messages" />
@if (this.provideCompilerMessages)
{
<MudTextField T="string" @bind-Text="@this.compilerMessages" Validation="@this.ValidatingCompilerMessages" AdornmentIcon="@Icons.Material.Filled.Error" Adornment="Adornment.Start" Label="Compiler messages" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
@@ -26,13 +24,7 @@
</MudStack>
<MudTextField T="string" @bind-Text="@this.questions" Validation="@this.ValidateQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionMark" Adornment="Adornment.Start" Label="Your question(s)" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" 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>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Color="Color.Info" OnClick="() => this.GetSupport()" Class="mb-3">
Get support
@@ -1,9 +1,11 @@
using System.Text;
namespace AIStudio.Components.Pages.Coding;
namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
protected override string Title => "Coding Assistant";
protected override string Description =>
@@ -24,6 +26,30 @@ public partial class AssistantCoding : AssistantBaseCore
When the user asks in a different language than English, you answer in the same language!
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override void ResetFrom()
{
this.codingContexts.Clear();
this.compilerMessages = string.Empty;
this.questions = string.Empty;
if (!this.MightPreselectValues())
{
this.provideCompilerMessages = false;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.Coding.PreselectOptions)
{
this.provideCompilerMessages = this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages;
return true;
}
return false;
}
private readonly List<CodingContext> codingContexts = new();
private bool provideCompilerMessages;
private string compilerMessages = string.Empty;
@@ -33,11 +59,9 @@ public partial class AssistantCoding : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
if (this.SettingsManager.ConfigurationData.PreselectCodingOptions)
{
this.provideCompilerMessages = this.SettingsManager.ConfigurationData.PreselectCodingCompilerMessages;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedCodingProvider);
}
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CODING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.questions = deferredContent;
await base.OnInitializedAsync();
}
@@ -68,11 +92,23 @@ public partial class AssistantCoding : AssistantBaseCore
this.codingContexts.Add(new()
{
Id = $"Context {this.codingContexts.Count + 1}",
Language = this.SettingsManager.ConfigurationData.PreselectCodingOptions ? this.SettingsManager.ConfigurationData.PreselectedCodingLanguage : default,
OtherLanguage = this.SettingsManager.ConfigurationData.PreselectCodingOptions ? this.SettingsManager.ConfigurationData.PreselectedCodingOtherLanguage : string.Empty,
Language = this.SettingsManager.ConfigurationData.Coding.PreselectOptions ? this.SettingsManager.ConfigurationData.Coding.PreselectedProgrammingLanguage : default,
OtherLanguage = this.SettingsManager.ConfigurationData.Coding.PreselectOptions ? this.SettingsManager.ConfigurationData.Coding.PreselectedOtherProgrammingLanguage : string.Empty,
});
}
private ValueTask DeleteContext(int index)
{
if(this.codingContexts.Count < index + 1)
return ValueTask.CompletedTask;
this.codingContexts.RemoveAt(index);
this.form?.ResetValidation();
this.StateHasChanged();
return ValueTask.CompletedTask;
}
private async Task GetSupport()
{
await this.form!.Validate();
@@ -87,9 +123,13 @@ public partial class AssistantCoding : AssistantBaseCore
foreach (var codingContext in this.codingContexts)
{
sbContext.AppendLine($"ID: {codingContext.Id}");
sbContext.AppendLine($"Language: {codingContext.Language.Name()}");
sbContext.AppendLine($"Other Language: {codingContext.OtherLanguage}");
sbContext.AppendLine($"Content:");
if(codingContext.Language is not CommonCodingLanguages.OTHER)
sbContext.AppendLine($"Language: {codingContext.Language.Name()}");
else
sbContext.AppendLine($"Language: {codingContext.OtherLanguage}");
sbContext.AppendLine("Content:");
sbContext.AppendLine("```");
sbContext.AppendLine(codingContext.Code);
sbContext.AppendLine("```");
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.Coding;
namespace AIStudio.Assistants.Coding;
public sealed class CodingContext(string id, CommonCodingLanguages language, string otherLanguage, string code)
{
@@ -2,7 +2,7 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages.Coding;
namespace AIStudio.Assistants.Coding;
public partial class CodingContextItem : ComponentBase
{
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.Coding;
namespace AIStudio.Assistants.Coding;
public static class CommonCodingLanguageExtensions
{
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.Coding;
namespace AIStudio.Assistants.Coding;
public enum CommonCodingLanguages
{
@@ -0,0 +1,27 @@
@attribute [Route(Routes.ASSISTANT_EMAIL)]
@inherits AssistantBaseCore
<MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" />
@if (this.provideHistory)
{
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<MudTextField T="string" @bind-Text="@this.inputHistory" Validation="@this.ValidateHistory" Label="Previous conversation" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Provide the previous conversation, e.g., the last e-mail, the last chat, etc." Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.DocumentScanner"/>
</MudPaper>
}
<MudTextField T="string" @bind-Text="@this.inputGreeting" Label="(Optional) The greeting phrase to use" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Placeholder="Dear Colleagues" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputBulletPoints" Validation="@this.ValidateBulletPoints" AdornmentIcon="@Icons.Material.Filled.ListAlt" Adornment="Adornment.Start" Label="Your bullet points" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Bullet list the content of the e-mail roughly. Use dashes (-) to separate the items." Immediate="@false" DebounceInterval="1_000" OnDebounceIntervalElapsed="@this.OnContentChanged" Placeholder="@PLACEHOLDER_BULLET_POINTS"/>
<MudSelect T="string" Label="(Optional) Are any of your points particularly important?" MultiSelection="@true" @bind-SelectedValues="@this.selectedFoci" Variant="Variant.Outlined" Class="mb-3" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.ListAlt">
@foreach (var contentLine in this.bulletPointsLines)
{
<MudSelectItem T="string" Value="@contentLine">@contentLine</MudSelectItem>
}
</MudSelect>
<MudTextField T="string" @bind-Text="@this.inputName" Label="(Optional) Your name for the closing salutation" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Your name for the closing salutation of your e-mail." Class="mb-3"/>
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Select the writing style" ValidateSelection="@this.ValidateWritingStyle"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.CreateMail()">
Create e-mail
</MudButton>
@@ -0,0 +1,245 @@
using System.Text;
using AIStudio.Chat;
namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
protected override string Title => "E-Mail";
protected override string Description =>
"""
Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input.
""";
protected override string SystemPrompt =>
$"""
You are an automated system that writes emails. {this.SystemPromptHistory()} The user provides you with bullet points on what
he want to address in the response. Regarding the writing style of the email: {this.selectedWritingStyle.Prompt()}
{this.SystemPromptGreeting()} {this.SystemPromptName()} You write the email in the following language: {this.SystemPromptLanguage()}.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputBulletPoints = string.Empty;
this.bulletPointsLines.Clear();
this.selectedFoci = [];
this.provideHistory = false;
this.inputHistory = string.Empty;
if (!this.MightPreselectValues())
{
this.inputName = string.Empty;
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.selectedWritingStyle = WritingStyles.NONE;
this.inputGreeting = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.EMail.PreselectOptions)
{
this.inputName = this.SettingsManager.ConfigurationData.EMail.SenderName;
this.inputGreeting = this.SettingsManager.ConfigurationData.EMail.Greeting;
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.EMail.PreselectOtherLanguage;
return true;
}
return false;
}
private const string PLACEHOLDER_BULLET_POINTS = """
- The last meeting was good
- Thank you for feedback
- Next is milestone 3
- I need your input by next Wednesday
""";
private WritingStyles selectedWritingStyle = WritingStyles.NONE;
private string inputGreeting = string.Empty;
private string inputBulletPoints = string.Empty;
private readonly List<string> bulletPointsLines = [];
private IEnumerable<string> selectedFoci = new HashSet<string>();
private string inputName = string.Empty;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
private bool provideHistory;
private string inputHistory = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_EMAIL_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputBulletPoints = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidateBulletPoints(string content)
{
if(string.IsNullOrWhiteSpace(content))
return "Please provide some content for the e-mail.";
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
if(!line.TrimStart().StartsWith('-'))
return "Please start each line of your content list with a dash (-) to create a bullet point list.";
return null;
}
private string? ValidateTargetLanguage(CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Please select a target language for the e-mail.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string? ValidateWritingStyle(WritingStyles style)
{
if(style == WritingStyles.NONE)
return "Please select a writing style for the e-mail.";
return null;
}
private string? ValidateHistory(string history)
{
if(this.provideHistory && string.IsNullOrWhiteSpace(history))
return "Please provide some history for the e-mail.";
return null;
}
private void OnContentChanged(string content)
{
this.bulletPointsLines.Clear();
var previousSelectedFoci = new HashSet<string>();
foreach (var line in content.AsSpan().EnumerateLines())
{
var trimmedLine = line.Trim();
if (trimmedLine.StartsWith("-"))
trimmedLine = trimmedLine[1..].Trim();
if (trimmedLine.Length == 0)
continue;
var finalLine = trimmedLine.ToString();
if(this.selectedFoci.Any(x => x.StartsWith(finalLine, StringComparison.InvariantCultureIgnoreCase)))
previousSelectedFoci.Add(finalLine);
this.bulletPointsLines.Add(finalLine);
}
this.selectedFoci = previousSelectedFoci;
}
private string SystemPromptHistory()
{
if (this.provideHistory)
return "You receive the previous conversation as context.";
return string.Empty;
}
private string SystemPromptGreeting()
{
if(!string.IsNullOrWhiteSpace(this.inputGreeting))
return $"Your greeting should consider the following formulation: {this.inputGreeting}.";
return string.Empty;
}
private string SystemPromptName()
{
if(!string.IsNullOrWhiteSpace(this.inputName))
return $"For the closing phrase of the email, please use the following name: {this.inputName}.";
return string.Empty;
}
private string SystemPromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.AS_IS)
return "Use the same language as the input";
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private string PromptFoci()
{
if(!this.selectedFoci.Any())
return string.Empty;
var sb = new StringBuilder();
sb.AppendLine("I want to amplify the following points:");
foreach (var focus in this.selectedFoci)
sb.AppendLine($"- {focus}");
return sb.ToString();
}
private string PromptHistory()
{
if(!this.provideHistory)
return string.Empty;
return $"""
The previous conversation was:
```
{this.inputHistory}
```
""";
}
private async Task CreateMail()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.PromptHistory()}
My bullet points for the e-mail are:
{this.inputBulletPoints}
{this.PromptFoci()}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,11 @@
namespace AIStudio.Assistants.EMail;
public enum WritingStyles
{
NONE = 0,
BUSINESS_FORMAL,
BUSINESS_INFORMAL,
ACADEMIC,
PERSONAL,
}
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.EMail;
public static class WritingStylesExtensions
{
public static string Name(this WritingStyles style) => style switch
{
WritingStyles.ACADEMIC => "Academic",
WritingStyles.PERSONAL => "Personal",
WritingStyles.BUSINESS_FORMAL => "Business formal",
WritingStyles.BUSINESS_INFORMAL => "Business informal",
_ => "Not specified",
};
public static string Prompt(this WritingStyles style) => style switch
{
WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.",
WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.",
WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.",
WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.",
_ => "Use a formal business style for this e-mail.",
};
}
@@ -0,0 +1,10 @@
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.ProofreadText()">
Proofread
</MudButton>
@@ -0,0 +1,132 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;
protected override string Title => "Grammar & Spelling Checker";
protected override string Description =>
"""
Check the grammar and spelling of a text.
""";
protected override string SystemPrompt =>
$"""
You are an expert in languages and their rules. For example, you know how US and UK English or German in
Germany and German in Austria differ. You receive text as input. You check the spelling and grammar of
this text according to the rules of {this.SystemPromptLanguage()}. You never add information. You
never ask the user for additional information. You do not attempt to improve the wording of the text.
Your response includes only the corrected text. Do not explain your changes. If no changes are needed,
you return the text unchanged.
""";
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
protected override Func<string> Result2Copy => () => this.correctedText;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = Tools.Components.GRAMMAR_SPELLING_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.correctedText) ? this.inputText : this.correctedText
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
this.correctedText = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.GrammarSpelling.PreselectedOtherLanguage;
return true;
}
return false;
}
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private string correctedText = string.Empty;
private string? ValidateText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
var lang = this.selectedTargetLanguage switch
{
CommonLanguages.AS_IS => "the source language",
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}",
};
if (string.IsNullOrWhiteSpace(lang))
return "the source language";
return lang;
}
private async Task ProofreadText()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(this.inputText);
this.correctedText = await this.AddAIResponseAsync(time);
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText);
}
}
@@ -1,5 +1,4 @@
@page "/assistant/icons"
@using AIStudio.Settings
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
@@ -16,13 +15,7 @@
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
}
</MudStack>
<MudSelect T="Provider" @bind-Value="@this.providerSettings" 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>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
Find icon
@@ -1,24 +1,8 @@
namespace AIStudio.Components.Pages.IconFinder;
namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore
{
private string inputContext = string.Empty;
private IconSources selectedIconSource;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
if (this.SettingsManager.ConfigurationData.PreselectIconOptions)
{
this.selectedIconSource = this.SettingsManager.ConfigurationData.PreselectedIconSource;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedIconProvider);
}
await base.OnInitializedAsync();
}
#endregion
protected override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;
protected override string Title => "Icon Finder";
@@ -40,7 +24,45 @@ public partial class AssistantIconFinder : AssistantBaseCore
related to the keyword "buildings" might be the best match. Provide your keywords in a Markdown list without
quotation marks.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override void ResetFrom()
{
this.inputContext = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedIconSource = IconSources.GENERIC;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.IconFinder.PreselectOptions)
{
this.selectedIconSource = this.SettingsManager.ConfigurationData.IconFinder.PreselectedSource;
return true;
}
return false;
}
private string inputContext = string.Empty;
private IconSources selectedIconSource;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_ICON_FINDER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContext = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingContext(string context)
{
if(string.IsNullOrWhiteSpace(context))
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.IconFinder;
namespace AIStudio.Assistants.IconFinder;
public static class IconSourceExtensions
{
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.IconFinder;
namespace AIStudio.Assistants.IconFinder;
public enum IconSources
{
@@ -0,0 +1,15 @@
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
@inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
{
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions && this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)"/>
}
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Legal document" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="Your questions" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Disabled="@this.isAgentRunning" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.AksQuestions()">
Ask your questions
</MudButton>
@@ -0,0 +1,87 @@
namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;
protected override string Title => "Legal Check";
protected override string Description =>
"""
Provide a legal document and ask a question about it. This assistant does not
replace legal advice. Consult a lawyer to get professional advice. Remember
that LLMs can invent answers and facts. Please do not rely on this answers.
""";
protected override string SystemPrompt =>
"""
You are an expert in legal matters. You have studied international law and are familiar with the law in
various countries. You receive a legal document and answer the user's questions. You are a friendly and
professional assistant. You respond to the user in the language in which the questions were asked. If
you are unsure, unfamiliar with the legal area, or do not know the answers, you inform the user.
Never invent facts!
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override void ResetFrom()
{
this.inputLegalDocument = string.Empty;
this.inputQuestions = string.Empty;
this.MightPreselectValues();
}
protected override bool MightPreselectValues() => false;
private bool isAgentRunning;
private string inputLegalDocument = string.Empty;
private string inputQuestions = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_LEGAL_CHECK_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputQuestions = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingLegalDocument(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a legal document as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidatingQuestions(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide your questions as input.";
return null;
}
private async Task AksQuestions()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
# The legal document
{this.inputLegalDocument}
# The questions
{this.inputQuestions}
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,12 @@
@attribute [Route(Routes.ASSISTANT_REWRITE)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="Writing style" AllowOther="@false" />
<EnumSelection T="SentenceStructure" NameFunc="@(voice => voice.Name())" @bind-Value="@this.selectedSentenceStructure" Icon="@Icons.Material.Filled.Person4" Label="Sentence structure" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.RewriteText()">
Improve
</MudButton>
@@ -0,0 +1,139 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
protected override string Title => "Rewrite & Improve Text";
protected override string Description =>
"""
Rewrite and improve your text. Please note, that the capabilities of the different LLM providers will vary.
""";
protected override string SystemPrompt =>
$"""
You are an expert in language and style. You receive a text as input. First, you review the text. If no
changes are needed, you return the text without modifications. If a change is necessary, you improve the
text. You can also correct spelling and grammar issues. You never add additional information. You never
ask the user for additional information. Your response only contains the improved text. You do not explain
your changes. If no changes are needed, you return the text unchanged.
The style of the text: {this.selectedWritingStyle.Prompt()}.{this.selectedSentenceStructure.Prompt()}
You follow the rules according to {this.SystemPromptLanguage()} in all your changes.
""";
protected override bool ShowResult => false;
protected override bool ShowDedicatedProgress => true;
protected override Func<string> Result2Copy => () => this.rewrittenText;
protected override IReadOnlyList<IButtonData> FooterButtons =>
[
new SendToButton
{
Self = Tools.Components.REWRITE_ASSISTANT,
UseResultingContentBlockData = false,
GetText = () => string.IsNullOrWhiteSpace(this.rewrittenText) ? this.inputText : this.rewrittenText,
},
];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
this.rewrittenText = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.selectedWritingStyle = WritingStyles.NOT_SPECIFIED;
this.selectedSentenceStructure = SentenceStructure.NOT_SPECIFIED;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.RewriteImprove.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedOtherLanguage;
this.selectedWritingStyle = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedWritingStyle;
this.selectedSentenceStructure = this.SettingsManager.ConfigurationData.RewriteImprove.PreselectedSentenceStructure;
return true;
}
return false;
}
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_REWRITE_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private string rewrittenText = string.Empty;
private WritingStyles selectedWritingStyle;
private SentenceStructure selectedSentenceStructure;
private string? ValidateText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
var lang = this.selectedTargetLanguage switch
{
CommonLanguages.AS_IS => "the source language",
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}",
};
if (string.IsNullOrWhiteSpace(lang))
return "the source language";
return lang;
}
private async Task RewriteText()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(this.inputText);
this.rewrittenText = await this.AddAIResponseAsync(time);
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.rewrittenText);
}
}
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.RewriteImprove;
public enum SentenceStructure
{
NOT_SPECIFIED = 0,
ACTIVE,
PASSIVE,
}
@@ -0,0 +1,20 @@
namespace AIStudio.Assistants.RewriteImprove;
public static class SentenceStructureExtensions
{
public static string Name(this SentenceStructure sentenceStructure) => sentenceStructure switch
{
SentenceStructure.ACTIVE => "Active voice",
SentenceStructure.PASSIVE => "Passive voice",
_ => "Not Specified",
};
public static string Prompt(this SentenceStructure sentenceStructure) => sentenceStructure switch
{
SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.",
SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.",
_ => string.Empty,
};
}
@@ -0,0 +1,16 @@
namespace AIStudio.Assistants.RewriteImprove;
public enum WritingStyles
{
NOT_SPECIFIED = 0,
EVERYDAY,
BUSINESS,
SCIENTIFIC,
JOURNALISTIC,
LITERARY,
TECHNICAL,
MARKETING,
ACADEMIC,
LEGAL,
}
@@ -0,0 +1,34 @@
namespace AIStudio.Assistants.RewriteImprove;
public static class WritingStylesExtensions
{
public static string Name(this WritingStyles style) => style switch
{
WritingStyles.EVERYDAY => "Everyday (personal texts, social media)",
WritingStyles.BUSINESS => "Business (business emails, reports, presentations)",
WritingStyles.SCIENTIFIC => "Scientific (scientific papers, research reports)",
WritingStyles.JOURNALISTIC => "Journalistic (magazines, newspapers, news)",
WritingStyles.LITERARY => "Literary (fiction, poetry)",
WritingStyles.TECHNICAL => "Technical (manuals, documentation)",
WritingStyles.MARKETING => "Marketing (advertisements, sales texts)",
WritingStyles.ACADEMIC => "Academic (essays, seminar papers)",
WritingStyles.LEGAL => "Legal (legal texts, contracts)",
_ => "Not specified",
};
public static string Prompt(this WritingStyles style) => style switch
{
WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.",
WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.",
WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.",
WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.",
WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.",
WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.",
WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.",
WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.",
WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.",
_ => "Keep the style of the text as it is.",
};
}
@@ -0,0 +1,12 @@
@attribute [Route(Routes.ASSISTANT_SYNONYMS)]
@inherits AssistantBaseCore
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="Your word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindSynonyms()">
Get synonyms
</MudButton>
@@ -0,0 +1,164 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
protected override string Title => "Synonyms";
protected override string Description =>
"""
Find synonyms for words or phrases.
""";
protected override string SystemPrompt =>
$"""
You have a PhD in linguistics. Therefore, you are an expert in the {this.SystemPromptLanguage()} language.
You receive a word or phrase as input. You might also receive some context. You then provide
a list of synonyms as a Markdown list.
First, derive possible meanings from the word, phrase, and context, when available. Then, provide
possible synonyms for each meaning.
Example for the word "learn" and the language English (US):
Derive possible meanings (*this list is not part of the output*):
- Meaning "to learn"
- Meaning "to retain"
Next, provide possible synonyms for each meaning, which is your output:
# Meaning "to learn"
- absorb
- study
- acquire
- advance
- practice
# Meaning "to retain"
- remember
- note
- realize
You do not ask follow-up questions and never repeat the task instructions. When you do not know
any synonyms for the given word or phrase, you state this. Your output is always in
the {this.SystemPromptLanguage()} language.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
this.inputContext = string.Empty;
if (!this.MightPreselectValues())
{
this.selectedLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.Synonyms.PreselectOptions)
{
this.selectedLanguage = this.SettingsManager.ConfigurationData.Synonyms.PreselectedLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Synonyms.PreselectedOtherLanguage;
return true;
}
return false;
}
private string inputText = string.Empty;
private string inputContext = string.Empty;
private CommonLanguages selectedLanguage;
private string customTargetLanguage = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_SYNONYMS_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputContext = deferredContent;
await base.OnInitializedAsync();
}
#endregion
private string? ValidatingText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a word or phrase as input.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
var lang = this.selectedLanguage switch
{
CommonLanguages.AS_IS => "source",
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedLanguage.Name()}",
};
if (string.IsNullOrWhiteSpace(lang))
return "source";
return lang;
}
private string UserPromptContext()
{
if(string.IsNullOrWhiteSpace(this.inputContext))
return string.Empty;
return $"""
The given context is:
```
{this.inputContext}
```
""";
}
private async Task FindSynonyms()
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.UserPromptContext()}
The given word or phrase is:
```
{this.inputText}
```
""");
await this.AddAIResponseAsync(time);
}
}
@@ -0,0 +1,16 @@
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
@inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
{
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions && this.SettingsManager.ConfigurationData.TextSummarizer.PreselectContentCleanerAgent)"/>
}
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="Custom target language" ValidateOther="@this.ValidateCustomLanguage" />
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="Target complexity" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="Your expertise" ValidateOther="@this.ValidateExpertInField" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SummarizeText()" Disabled="@this.isAgentRunning">
Summarize
</MudButton>
@@ -1,9 +1,11 @@
using AIStudio.Tools;
using AIStudio.Chat;
namespace AIStudio.Components.Pages.TextSummarizer;
namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;
protected override string Title => "Text Summarizer";
protected override string Description =>
@@ -23,7 +25,41 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
a summary with the requested complexity. In any case, do not add any information.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
if(!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
this.selectedComplexity = Complexity.NO_CHANGE;
this.expertInField = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.TextSummarizer.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedOtherLanguage;
this.selectedComplexity = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedComplexity;
this.expertInField = this.SettingsManager.ConfigurationData.TextSummarizer.PreselectedExpertInField;
return true;
}
return false;
}
private string inputText = string.Empty;
private bool isAgentRunning;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private Complexity selectedComplexity;
@@ -33,14 +69,9 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
if(this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.PreselectedTextSummarizerTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.PreselectedTextSummarizerOtherLanguage;
this.selectedComplexity = this.SettingsManager.ConfigurationData.PreselectedTextSummarizerComplexity;
this.expertInField = this.SettingsManager.ConfigurationData.PreselectedTextSummarizerExpertInField;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedTextSummarizerProvider);
}
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
await base.OnInitializedAsync();
}
@@ -80,7 +111,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
{this.selectedTargetLanguage.PromptSummarizing(this.customTargetLanguage)}
{this.selectedComplexity.Prompt(this.expertInField)}
Please summarize the following text:
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.TextSummarizer;
namespace AIStudio.Assistants.TextSummarizer;
public enum Complexity
{
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Pages.TextSummarizer;
namespace AIStudio.Assistants.TextSummarizer;
public static class ComplexityExtensions
{
@@ -0,0 +1,24 @@
@attribute [Route(Routes.ASSISTANT_TRANSLATION)]
@inherits AssistantBaseCore
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
{
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectWebContentReader)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.Translation.PreselectOptions && this.SettingsManager.ConfigurationData.Translation.PreselectContentCleanerAgent)"/>
}
<MudTextSwitch Label="Live translation" @bind-Value="@this.liveTranslation" LabelOn="Live translation" LabelOff="No live translation"/>
@if (this.liveTranslation)
{
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" Immediate="@true" DebounceInterval="@this.SettingsManager.ConfigurationData.Translation.DebounceIntervalMilliseconds" OnDebounceIntervalElapsed="() => this.TranslateText(force: false)" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
else
{
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidatingTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Disabled="@this.isAgentRunning" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.TranslateText(force: true)">
Translate
</MudButton>
@@ -1,9 +1,11 @@
using AIStudio.Tools;
using AIStudio.Chat;
namespace AIStudio.Components.Pages.Translation;
namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore
{
protected override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
protected override string Title => "Translation";
protected override string Description =>
@@ -19,7 +21,40 @@ public partial class AssistantTranslation : AssistantBaseCore
language requires, e.g., shorter sentences, you should split the text into shorter sentences.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
{
this.inputText = string.Empty;
this.inputTextLastTranslation = string.Empty;
if (!this.MightPreselectValues())
{
this.liveTranslation = false;
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.Translation.PreselectOptions)
{
this.liveTranslation = this.SettingsManager.ConfigurationData.Translation.PreselectLiveTranslation;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.Translation.PreselectOtherLanguage;
return true;
}
return false;
}
private bool liveTranslation;
private bool isAgentRunning;
private string inputText = string.Empty;
private string inputTextLastTranslation = string.Empty;
private CommonLanguages selectedTargetLanguage;
@@ -29,13 +64,9 @@ public partial class AssistantTranslation : AssistantBaseCore
protected override async Task OnInitializedAsync()
{
if (this.SettingsManager.ConfigurationData.PreselectTranslationOptions)
{
this.liveTranslation = this.SettingsManager.ConfigurationData.PreselectLiveTranslation;
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.PreselectedTranslationTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.PreselectTranslationOtherLanguage;
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedTranslationProvider);
}
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_TRANSLATION_ASSISTANT).FirstOrDefault();
if (deferredContent is not null)
this.inputText = deferredContent;
await base.OnInitializedAsync();
}
@@ -65,9 +96,10 @@ public partial class AssistantTranslation : AssistantBaseCore
return null;
}
private async Task TranslateText(bool force)
{
await this.form!.Validate();
if (!this.inputIsValid)
return;
@@ -78,7 +110,7 @@ public partial class AssistantTranslation : AssistantBaseCore
this.CreateChatThread();
var time = this.AddUserRequest(
$"""
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
{this.selectedTargetLanguage.PromptTranslation(this.customTargetLanguage)}
The given text is:
@@ -0,0 +1,4 @@
@using AIStudio
@using AIStudio.Tools
@using AIStudio.Components
@using MudBlazor
+1
View File
@@ -11,6 +11,7 @@ public enum ChatRole
SYSTEM,
USER,
AI,
AGENT,
}
/// <summary>
+1 -1
View File
@@ -3,7 +3,7 @@ namespace AIStudio.Chat;
/// <summary>
/// Data structure for a chat thread.
/// </summary>
public sealed class ChatThread
public sealed record ChatThread
{
/// <summary>
/// The unique identifier of the chat thread.
@@ -1,7 +1,7 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Chat;
/// <summary>
@@ -40,10 +40,7 @@ public partial class ContentBlockComponent : ComponentBase
public string Class { get; set; } = string.Empty;
[Inject]
private Rust Rust { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
@@ -100,7 +97,7 @@ public partial class ContentBlockComponent : ComponentBase
{
case ContentType.TEXT:
var textContent = (ContentText) this.Content;
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, textContent.Text);
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
break;
default:
+1 -1
View File
@@ -29,7 +29,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
{
throw new NotImplementedException();
}
+6 -6
View File
@@ -35,18 +35,18 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public async Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
{
if(chatThread is null)
return;
// Store the last time we got a response. We use this later,
// Store the last time we got a response. We use this ater
// to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode
// the user chose.
var last = DateTimeOffset.Now;
// Start another thread by using a task, to uncouple
// Start another thread by using a task to uncouple
// the UI thread from the AI processing:
await Task.Run(async () =>
{
@@ -54,7 +54,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var deltaText in provider.StreamChatCompletion(jsRuntime, settings, chatModel, chatThread, token))
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
{
// When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested)
@@ -70,7 +70,7 @@ public sealed class ContentText : IContent
// Notify the UI that the content has changed,
// depending on the energy saving mode:
var now = DateTimeOffset.Now;
switch (settings.ConfigurationData.IsSavingEnergy)
switch (settings.ConfigurationData.App.IsSavingEnergy)
{
// Energy saving mode is off. We notify the UI
// as fast as possible -- no matter the odds:
@@ -89,7 +89,7 @@ public sealed class ContentText : IContent
}
// Stop the waiting animation (in case the loop
// was stopped or no content was received):
// was stopped, or no content was received):
this.InitialRemoteWait = false;
this.IsStreaming = false;
}, token);
+1 -1
View File
@@ -42,5 +42,5 @@ public interface IContent
/// <summary>
/// Uses the provider to create the content.
/// </summary>
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
}
@@ -0,0 +1,6 @@
namespace AIStudio.Chat;
public static class SystemPrompts
{
public const string DEFAULT = "You are a helpful assistant!";
}
@@ -1,27 +0,0 @@
@* ReSharper disable Html.PathError *@
<!DOCTYPE html>
<html lang="en">
<head>
<title>MindWork AI Studio</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="icon" type="image/png" href="favicon.png"/>
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<link href="app.css" rel="stylesheet" />
<HeadOutlet/>
</head>
<body style="overflow: hidden;">
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)"/>
<script src="_framework/blazor.web.js" autostart="false"></script>
<script src="boot.js"></script>
<script src="system/MudBlazor/MudBlazor.min.js"></script>
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
<script src="app.js"></script>
</body>
</html>
@@ -1,25 +0,0 @@
@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>
<Issues IssuesData="@this.inputIssues"/>
@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>
@@ -1,122 +0,0 @@
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 abstract string Title { get; }
protected abstract string Description { get; }
protected abstract string SystemPrompt { get; }
private protected virtual RenderFragment? Body => null;
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form;
protected bool inputIsValid;
private ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
#region Overrides of ComponentBase
protected override async Task OnParametersSetAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnParametersSetAsync();
}
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 string? ValidatingProvider(AIStudio.Settings.Provider provider)
{
if(provider.UsedProvider == Providers.NONE)
return "Please select a provider.";
return null;
}
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.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
}
}
@@ -0,0 +1,24 @@
<MudCard Outlined="@true" Style="border-width: 2px; border-color: #0d47a1; border-radius: 12px; border-style: solid; max-width: 20em;">
<MudCardHeader>
<CardHeaderContent>
<MudStack AlignItems="AlignItems.Center" Row="@true">
<MudIcon Icon="@this.Icon" Size="Size.Large" Color="Color.Primary"/>
<MudText Typo="Typo.h6">
@this.Name
</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack>
<MudText>
@this.Description
</MudText>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
@this.ButtonText
</MudButton>
</MudCardActions>
</MudCard>
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class AssistantBlock : ComponentBase
{
@@ -1,26 +0,0 @@
<MudItem xs="3">
<MudCard Outlined="@true" Style="border-width: 2px; border-color: #0d47a1; border-radius: 12px; border-style: solid;">
<MudCardHeader>
<CardHeaderContent>
<MudStack AlignItems="AlignItems.Center" Row="@true">
<MudIcon Icon="@this.Icon" Size="Size.Large" Color="Color.Primary"/>
<MudText Typo="Typo.h6">
@this.Name
</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack>
<MudText>
@this.Description
</MudText>
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
@this.ButtonText
</MudButton>
</MudCardActions>
</MudCard>
</MudItem>
@@ -1 +0,0 @@
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="Select a provider that is preselected." Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
@@ -1,11 +0,0 @@
<MudExpansionPanel Class="border-solid border rounded-lg" Expanded="@this.IsExpanded">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/>
<MudText Typo="Typo.h6">@this.HeaderText</MudText>
</div>
</TitleContent>
<ChildContent>
@this.ChildContent
</ChildContent>
</MudExpansionPanel>
@@ -1,3 +0,0 @@
namespace AIStudio.Components.Blocks;
public interface ITreeItem;
@@ -1,9 +0,0 @@
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
@this.ChildContent
</div>
@this.FooterContent
</div>
@@ -1,3 +0,0 @@
namespace AIStudio.Components.Blocks;
public readonly record struct TreeDivider : ITreeItem;
@@ -1,4 +1,4 @@
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class Changelog
{
@@ -13,6 +13,19 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
new (180, "v0.9.5, build 180 (2024-09-07 16:33 UTC)", "v0.9.5.md"),
new (179, "v0.9.4, build 179 (2024-09-06 20:11 UTC)", "v0.9.4.md"),
new (178, "v0.9.3, build 178 (2024-09-06 11:06 UTC)", "v0.9.3.md"),
new (177, "v0.9.2, build 177 (2024-09-05 09:19 UTC)", "v0.9.2.md"),
new (176, "v0.9.1, build 176 (2024-09-04 13:48 UTC)", "v0.9.1.md"),
new (175, "v0.9.0, build 175 (2024-09-03 16:00 UTC)", "v0.9.0.md"),
new (174, "v0.8.12, build 174 (2024-08-24 08:30 UTC)", "v0.8.12.md"),
new (173, "v0.8.11, build 173 (2024-08-21 07:03 UTC)", "v0.8.11.md"),
new (172, "v0.8.10, build 172 (2024-08-18 19:44 UTC)", "v0.8.10.md"),
new (171, "v0.8.9, build 171 (2024-08-18 10:35 UTC)", "v0.8.9.md"),
new (170, "v0.8.8, build 170 (2024-08-14 06:30 UTC)", "v0.8.8.md"),
new (169, "v0.8.7, build 169 (2024-08-01 19:08 UTC)", "v0.8.7.md"),
new (168, "v0.8.6, build 168 (2024-08-01 19:50 UTC)", "v0.8.6.md"),
new (167, "v0.8.5, build 167 (2024-07-28 16:44 UTC)", "v0.8.5.md"),
new (166, "v0.8.4, build 166 (2024-07-26 06:53 UTC)", "v0.8.4.md"),
new (165, "v0.8.3, build 165 (2024-07-25 13:25 UTC)", "v0.8.3.md"),
@@ -1,4 +1,3 @@
@using AIStudio.Tools
<MudSelect T="Log" @bind-Value="@this.SelectedLog" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Schedule" Margin="Margin.Dense" Label="Changelog" Class="mb-2 rounded-lg" Variant="Variant.Outlined" SelectedValuesChanged="() => this.ReadLogAsync()" OnKeyUp="() => this.ReadLogAsync()">
@foreach (var log in LOGS)
{
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class Changelog : ComponentBase
{
@@ -1,5 +1,4 @@
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
/// <summary>
/// Configuration component for any boolean option.
@@ -0,0 +1 @@
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="@this.HelpText()" Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
@@ -1,9 +1,8 @@
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class ConfigurationProviderSelection : ComponentBase, IMessageBusReceiver, IDisposable
{
@@ -22,6 +21,9 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
[Parameter]
public Func<bool> Disabled { get; set; } = () => false;
[Parameter]
public Func<string> HelpText { get; set; } = () => "Select a provider that is preselected.";
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
@@ -2,7 +2,7 @@ using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
/// <summary>
/// Configuration component for selecting a value from a list.
@@ -2,7 +2,7 @@ using System.Numerics;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class ConfigurationSlider<T> : ConfigurationBase where T : struct, INumber<T>
{
@@ -42,10 +42,35 @@ public partial class ConfigurationSlider<T> : ConfigurationBase where T : struct
[Parameter]
public Action<T> ValueUpdate { get; set; } = _ => { };
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
await this.EnsureMinMax();
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
await this.EnsureMinMax();
await base.OnParametersSetAsync();
}
#endregion
private async Task OptionChanged(T updatedValue)
{
this.ValueUpdate(updatedValue);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
private async Task EnsureMinMax()
{
if (this.Value() < this.Min)
await this.OptionChanged(this.Min);
else if(this.Value() > this.Max)
await this.OptionChanged(this.Max);
}
}
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class ConfigurationText : ConfigurationBase
{
@@ -0,0 +1,17 @@
@typeparam T
@inherits EnumSelectionBase
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection">
@foreach (var value in Enum.GetValues<T>())
{
<MudSelectItem Value="@value">
@this.NameFunc(value)
</MudSelectItem>
}
</MudSelect>
@if (this.AllowOther && this.Value.Equals(this.OtherValue))
{
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherInputChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/>
}
</MudStack>
@@ -0,0 +1,79 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class EnumSelection<T> : EnumSelectionBase where T : struct, Enum
{
[Parameter]
public T Value { get; set; }
[Parameter]
public EventCallback<T> ValueChanged { get; set; }
[Parameter]
public bool AllowOther { get; set; }
[Parameter]
public T OtherValue { get; set; }
[Parameter]
public string OtherInput { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> OtherInputChanged { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string LabelOther { get; set; } = "Other";
[Parameter]
public Func<T, string?> ValidateSelection { get; set; } = _ => null;
[Parameter]
public Func<string, string?> ValidateOther { get; set; } = _ => null;
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown;
/// <summary>
/// Gets or sets the custom name function for selecting the display name of an enum value.
/// </summary>
/// <typeparam name="T">The enum type.</typeparam>
/// <param name="value">The enum value.</param>
/// <returns>The display name of the enum value.</returns>
[Parameter]
public Func<T, string> NameFunc { get; set; } = value => value.ToString();
[Parameter]
public Func<T, Task> SelectionUpdated { get; set; } = _ => Task.CompletedTask;
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private async Task SelectionChanged(T value)
{
await this.ValueChanged.InvokeAsync(value);
await this.SelectionUpdated(value);
}
private async Task OtherValueChanged(string value)
{
await this.OtherInputChanged.InvokeAsync(value);
await this.SelectionUpdated(this.Value);
}
}
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public abstract class EnumSelectionBase : ComponentBase
{
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
}
@@ -0,0 +1,25 @@
<MudExpansionPanel Class="border-solid border rounded-lg" Expanded="@this.IsExpanded">
<TitleContent>
<div class="d-flex align-center">
<MudIcon Icon="@this.HeaderIcon" Size="@this.IconSize" Color="@this.IconColor" class="mr-3"/>
<MudText Typo="Typo.h6">@this.HeaderText</MudText>
@if (this.ShowEndButton)
{
<MudSpacer/>
if (!string.IsNullOrWhiteSpace(this.EndButtonTooltip))
{
<MudTooltip Text="@this.EndButtonTooltip">
<MudIconButton Icon="@this.EndButtonIcon" Color="@this.EndButtonColor" Size="Size.Medium" OnClick="async () => await this.EndButtonClickAsync()"/>
</MudTooltip>
}
else
{
<MudIconButton Icon="@this.EndButtonIcon" Color="@this.EndButtonColor" Size="Size.Medium" OnClick="async () => await this.EndButtonClickAsync()"/>
}
}
</div>
</TitleContent>
<ChildContent>
@this.ChildContent
</ChildContent>
</MudExpansionPanel>
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class ExpansionPanel : ComponentBase
{
@@ -21,4 +21,19 @@ public partial class ExpansionPanel : ComponentBase
[Parameter]
public bool IsExpanded { get; set; }
[Parameter]
public bool ShowEndButton { get; set; }
[Parameter]
public Func<ValueTask> EndButtonClickAsync { get; set; } = () => ValueTask.CompletedTask;
[Parameter]
public string EndButtonIcon { get; set; } = Icons.Material.Filled.Delete;
[Parameter]
public Color EndButtonColor { get; set; } = Color.Error;
[Parameter]
public string EndButtonTooltip { get; set; } = string.Empty;
}
@@ -0,0 +1,3 @@
namespace AIStudio.Components;
public interface ITreeItem;
@@ -0,0 +1,18 @@
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
@this.ChildContent
<div @ref="@this.AnchorAfterChildContent">
&nbsp;
</div>
</div>
@if (this.FooterContent is not null)
{
<MudPaper Class="pa-3 border-solid border rounded-lg">
@this.FooterContent
</MudPaper>
}
</div>
@@ -1,9 +1,8 @@
using AIStudio.Components.Layout;
using AIStudio.Tools;
using AIStudio.Layout;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class InnerScrolling : MSGComponentBase
{
@@ -26,6 +25,11 @@ public partial class InnerScrolling : MSGComponentBase
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
private ElementReference AnchorAfterChildContent { get; set; }
#region Overrides of ComponentBase
@@ -59,4 +63,9 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
public async Task ScrollToBottom()
{
await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime);
}
}
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class Issues : ComponentBase
{
@@ -1,3 +0,0 @@
namespace AIStudio.Components.Layout;
public record NavBarItem(string Name, string Icon, Color IconColor, string Path, bool MatchAll);
@@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class Motivation : ComponentBase
{
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
namespace AIStudio.Components;
public partial class MudTextList : ComponentBase
{
@@ -0,0 +1,7 @@
@typeparam T
<MudField Label="@this.Label" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.Disabled()">
<MudSlider T="@T" Size="Size.Medium" Value="@this.Value" ValueChanged="@this.ValueUpdated" Min="@this.Min" Max="@this.Max" Step="@this.Step" Immediate="@true" Disabled="@this.Disabled()">
@this.Value @this.Unit
</MudSlider>
</MudField>
@@ -0,0 +1,78 @@
using System.Numerics;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class MudTextSlider<T> : ComponentBase where T : struct, INumber<T>
{
/// <summary>
/// The minimum value for the slider.
/// </summary>
[Parameter]
public T Min { get; set; } = T.Zero;
/// <summary>
/// The maximum value for the slider.
/// </summary>
[Parameter]
public T Max { get; set; } = T.One;
/// <summary>
/// The step size for the slider.
/// </summary>
[Parameter]
public T Step { get; set; } = T.One;
/// <summary>
/// The unit to display next to the slider's value.
/// </summary>
[Parameter]
public string Unit { get; set; } = string.Empty;
[Parameter]
public T Value { get; set; }
[Parameter]
public EventCallback<T> ValueChanged { get; set; }
/// <summary>
/// The label to display above the slider.
/// </summary>
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public Func<bool> Disabled { get; set; } = () => false;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
await this.EnsureMinMax();
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
await this.EnsureMinMax();
await base.OnParametersSetAsync();
}
#endregion
private async Task EnsureMinMax()
{
if (this.Value < this.Min)
await this.ValueUpdated(this.Min);
else if(this.Value > this.Max)
await this.ValueUpdated(this.Max);
}
private async Task ValueUpdated(T value)
{
this.Value = value;
await this.ValueChanged.InvokeAsync(this.Value);
}
}
@@ -0,0 +1,5 @@
<MudField Label="@this.Label" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.Disabled">
<MudSwitch T="bool" Value="@this.Value" ValueChanged="@this.ValueChanged" Color="@this.Color" Validation="@this.Validation" Disabled="@this.Disabled">
@(this.Value ? this.LabelOn : this.LabelOff)
</MudSwitch>
</MudField>
@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class MudTextSwitch : ComponentBase
{
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public bool Value { get; set; }
[Parameter]
public EventCallback<bool> ValueChanged { get; set; }
[Parameter]
public Color Color { get; set; } = Color.Primary;
[Parameter]
public Func<bool, string?> Validation { get; set; } = _ => null;
[Parameter]
public string LabelOn { get; set; } = string.Empty;
[Parameter]
public string LabelOff { get; set; } = string.Empty;
}
Loaded 100 of 257 files, more files were not shown because too many files have changed in this diff. Show more