mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-19 23:38:00 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab0b855769 | ||
|
|
4cb49d9f27 | ||
|
|
2cea53d2a9 | ||
|
|
155c2d72f8 | ||
|
|
60febfec86 | ||
|
|
76dd08ee11 | ||
|
|
c1b92a05fc | ||
|
|
cac2373269 | ||
|
|
f3aa38e090 | ||
|
|
b3b8bf23dc | ||
|
|
a1c4618796 | ||
|
|
0f06b96943 | ||
|
|
d6bd0dc45b | ||
|
|
426f223a84 | ||
|
|
79892eb829 | ||
|
|
d6cf65e973 | ||
|
|
9267ef865d | ||
|
|
4f5815272a | ||
|
|
5250e5d2fb | ||
|
|
a956b55d6b | ||
|
|
a9ca51377e | ||
|
|
880f86316b | ||
|
|
063fd0fcfa | ||
|
|
179f350af5 | ||
|
|
58c9d8ac33 | ||
|
|
88aefe1c4e | ||
|
|
12e31453e3 | ||
|
|
4f823b0fe4 | ||
|
|
59d0321625 | ||
|
|
29263660fc | ||
|
|
6cc1d37db8 | ||
|
|
b29e181ecd | ||
|
|
9b1649b48a | ||
|
|
15e8fbc6ca | ||
|
|
d6e80a4563 |
No files matched your search
@@ -5,14 +5,14 @@ MindWork AI Studio is a desktop application available for macOS, Windows, and Li
|
||||
|
||||
**Key advantages:**
|
||||
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
|
||||
- **Independence**: Users are not tied to any single provider. The initial version supports OpenAI models (like GPT-4o, GPT-4, GPT-4 Turbo, etc.). Future versions will support other providers such as Mistral or Google Gemini.
|
||||
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), or [Fireworks](https://fireworks.ai/). Support for Google Gemini, and [Replicate](https://replicate.com/) is planned.
|
||||
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
|
||||
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
|
||||
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API.
|
||||
- **Flexibility**: Choose the provider and model best suited for your current task.
|
||||
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
|
||||
|
||||
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
||||
## **Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
|
||||
|
||||
## Support the Project
|
||||
Thank you for using MindWork AI Studio and considering supporting its development 😀. Your support helps keep the project alive and ensures continuous improvements and new features.
|
||||
@@ -30,9 +30,7 @@ Your support, whether big or small, keeps the wheels turning and is deeply appre
|
||||
|
||||
## Planned Features
|
||||
Here's an exciting look at some of the features we're planning to add to MindWork AI Studio in future releases:
|
||||
- **More providers**: We plan to add support for additional LLM providers, such as Mistral and Google Gemini, giving you more options to choose from.
|
||||
- **Persistent chats**: Your chats will be stored locally, allowing you to continue conversations at any time without starting from scratch.
|
||||
- **Local LLMs**: We aim to support local LLMs, enabling you to use options like LM Studio, `ollama`, or `llama.cpp` for a more private and self-contained experience.
|
||||
- **More providers**: We plan to add support for additional LLM providers, such as Google Gemini, giving you more options to choose from.
|
||||
- **System prompts**: Integration of a system prompt library will allow you to control the behavior of the LLM with predefined prompts, ensuring consistency and efficiency.
|
||||
- **Text replacement for better privacy**: Define keywords that will be replaced in your chats before sending content to the provider, enhancing your privacy.
|
||||
- **Advanced interactions**: We're full of ideas for advanced interactions tailored for specific use cases, whether in a business context or for writers and other professionals.
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String></wpf:ResourceDictionary>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,109 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
|
||||
namespace AIStudio.Agents;
|
||||
|
||||
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
|
||||
{
|
||||
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||
|
||||
protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
|
||||
|
||||
protected ThreadSafeRandom RNG { get; init; } = rng;
|
||||
|
||||
/// <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.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Agents;
|
||||
|
||||
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, 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: 
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -11,6 +11,7 @@ public enum ChatRole
|
||||
SYSTEM,
|
||||
USER,
|
||||
AI,
|
||||
AGENT,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,29 +3,35 @@ namespace AIStudio.Chat;
|
||||
/// <summary>
|
||||
/// Data structure for a chat thread.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the chat thread.</param>
|
||||
/// <param name="seed">The seed for the chat thread. Some providers use this to generate deterministic results.</param>
|
||||
/// <param name="systemPrompt">The system prompt for the chat thread.</param>
|
||||
/// <param name="blocks">The content blocks of the chat thread.</param>
|
||||
public sealed class ChatThread(string name, int seed, string systemPrompt, IEnumerable<ContentBlock> blocks)
|
||||
public sealed class ChatThread
|
||||
{
|
||||
/// <summary>
|
||||
/// The unique identifier of the chat thread.
|
||||
/// </summary>
|
||||
public Guid ChatId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier of the workspace.
|
||||
/// </summary>
|
||||
public Guid WorkspaceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = name;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The seed for the chat thread. Some providers use this to generate deterministic results.
|
||||
/// </summary>
|
||||
public int Seed { get; set; } = seed;
|
||||
public int Seed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The current system prompt for the chat thread.
|
||||
/// </summary>
|
||||
public string SystemPrompt { get; set; } = systemPrompt;
|
||||
|
||||
public string SystemPrompt { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The content blocks of the chat thread.
|
||||
/// </summary>
|
||||
public List<ContentBlock> Blocks { get; init; } = blocks.ToList();
|
||||
public List<ContentBlock> Blocks { get; init; } = [];
|
||||
}
|
||||
@@ -3,25 +3,22 @@ namespace AIStudio.Chat;
|
||||
/// <summary>
|
||||
/// A block of content in a chat thread. Might be any type of content, e.g., text, image, voice, etc.
|
||||
/// </summary>
|
||||
/// <param name="time">Time when the content block was created.</param>
|
||||
/// <param name="type">Type of the content block, e.g., text, image, voice, etc.</param>
|
||||
/// <param name="content">The content of the block.</param>
|
||||
public class ContentBlock(DateTimeOffset time, ContentType type, IContent content)
|
||||
public class ContentBlock
|
||||
{
|
||||
/// <summary>
|
||||
/// Time when the content block was created.
|
||||
/// </summary>
|
||||
public DateTimeOffset Time => time;
|
||||
public DateTimeOffset Time { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the content block, e.g., text, image, voice, etc.
|
||||
/// </summary>
|
||||
public ContentType ContentType => type;
|
||||
public ContentType ContentType { get; init; } = ContentType.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The content of the block.
|
||||
/// </summary>
|
||||
public IContent Content => content;
|
||||
public IContent? Content { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// The role of the content block in the chat thread, e.g., user, AI, etc.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@using AIStudio.Tools
|
||||
@using MudBlazor
|
||||
|
||||
<MudCard Class="my-2 rounded-lg" Outlined="@true">
|
||||
<MudCard Class="@this.CardClasses" Outlined="@true">
|
||||
<MudCardHeader>
|
||||
<CardHeaderAvatar>
|
||||
<MudAvatar Color="@this.Role.ToColor()">
|
||||
|
||||
@@ -33,6 +33,12 @@ public partial class ContentBlockComponent : ComponentBase
|
||||
[Parameter]
|
||||
public DateTimeOffset Time { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional CSS classes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private Rust Rust { get; init; } = null!;
|
||||
|
||||
@@ -107,4 +113,6 @@ public partial class ContentBlockComponent : ComponentBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
@@ -11,15 +13,19 @@ public sealed class ContentImage : IContent
|
||||
#region Implementation of IContent
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public bool InitialRemoteWait { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public bool IsStreaming { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
@@ -17,14 +19,19 @@ public sealed class ContentText : IContent
|
||||
#region Implementation of IContent
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public bool InitialRemoteWait { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
// [JsonIgnore]
|
||||
public bool IsStreaming { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingDone { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
@@ -6,6 +8,8 @@ namespace AIStudio.Chat;
|
||||
/// <summary>
|
||||
/// The interface for any content in the chat.
|
||||
/// </summary>
|
||||
[JsonDerivedType(typeof(ContentText), typeDiscriminator: "text")]
|
||||
[JsonDerivedType(typeof(ContentImage), typeDiscriminator: "image")]
|
||||
public interface IContent
|
||||
{
|
||||
/// <summary>
|
||||
@@ -13,22 +17,26 @@ public interface IContent
|
||||
/// Does not indicate that the stream is finished; it only indicates that we are
|
||||
/// waiting for the first response, i.e., wait for the remote to pick up the request.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool InitialRemoteWait { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the content is streaming right now. False, if the content is
|
||||
/// either static or the stream has finished.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsStreaming { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the content was changed during streaming.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingEvent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// An action that is called when the streaming is done.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Func<Task> StreamingDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
@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>
|
||||
@@ -0,0 +1,123 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
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 ThreadSafeRandom RNG { get; init; } = 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,19 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
//
|
||||
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
|
||||
//
|
||||
|
||||
public abstract class AssistantBaseCore : AssistantBase
|
||||
{
|
||||
private protected sealed override RenderFragment Body => this.BuildRenderTree;
|
||||
|
||||
// Allow content to be provided by a .razor file but without
|
||||
// overriding the content of the base class
|
||||
protected new virtual void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class AssistantBlock : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.DisabledByDefault;
|
||||
|
||||
[Parameter]
|
||||
public string ButtonText { get; set; } = "Start";
|
||||
|
||||
[Parameter]
|
||||
public string Link { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -13,8 +13,21 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
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"),
|
||||
new (164, "v0.8.2, build 164 (2024-07-16 18:03 UTC)", "v0.8.2.md"),
|
||||
new (163, "v0.8.1, build 163 (2024-07-16 08:32 UTC)", "v0.8.1.md"),
|
||||
new (162, "v0.8.0, build 162 (2024-07-14 19:39 UTC)", "v0.8.0.md"),
|
||||
new (161, "v0.7.1, build 161 (2024-07-13 11:42 UTC)", "v0.7.1.md"),
|
||||
new (160, "v0.7.0, build 160 (2024-07-13 08:21 UTC)", "v0.7.0.md"),
|
||||
new (159, "v0.6.3, build 159 (2024-07-03 18:26 UTC)", "v0.6.3.md"),
|
||||
new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"),
|
||||
new (157, "v0.6.1, build 157 (2024-06-30 19:00 UTC)", "v0.6.1.md"),
|
||||
new (156, "v0.6.0, build 156 (2024-06-30 12:49 UTC)", "v0.6.0.md"),
|
||||
new (155, "v0.5.2, build 155 (2024-06-25 18:07 UTC)", "v0.5.2.md"),
|
||||
new (154, "v0.5.1, build 154 (2024-06-25 15:35 UTC)", "v0.5.1.md"),
|
||||
new (154, "v0.5.2, build 154 (2024-06-25 15:35 UTC)", "v0.5.2.md"),
|
||||
new (149, "v0.5.0, build 149 (2024-06-02 18:51 UTC)", "v0.5.0.md"),
|
||||
new (138, "v0.4.0, build 138 (2024-05-26 13:26 UTC)", "v0.4.0.md"),
|
||||
new (120, "v0.3.0, build 120 (2024-05-18 21:57 UTC)", "v0.3.0.md"),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
@inherits ConfigurationBase
|
||||
|
||||
<MudField Disabled="@this.Disabled()" Label="@this.OptionDescription" Variant="Variant.Outlined" HelperText="@this.OptionHelp" Class="@MARGIN_CLASS">
|
||||
<MudSwitch T="bool" Disabled="@this.Disabled()" Value="@this.State()" ValueChanged="@this.OptionChanged" Color="Color.Primary">
|
||||
@(this.State() ? this.LabelOn : this.LabelOff)
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration component for any boolean option.
|
||||
@@ -35,5 +35,6 @@ public partial class ConfigurationOption : ConfigurationBase
|
||||
{
|
||||
this.StateUpdate(updatedState);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ConfigurationSelect OptionDescription="Preselected provider" Disabled="@this.Disabled" OptionHelp="Select a provider that is preselected." Data="@this.Data" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
|
||||
@@ -0,0 +1,83 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class ConfigurationProviderSelection : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
[Parameter]
|
||||
public Func<string> SelectedValue { get; set; } = () => string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Action<string> SelectionUpdate { get; set; } = _ => { };
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<ConfigurationSelectData<string>> Data { get; set; } = new List<ConfigurationSelectData<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Is the selection component disabled?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<bool> Disabled { get; set; } = () => false;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
|
||||
if(string.IsNullOrWhiteSpace(this.SelectedValue()))
|
||||
break;
|
||||
|
||||
// Check if the selected value is still valid:
|
||||
if (this.Data.All(x => x.Value != this.SelectedValue()))
|
||||
{
|
||||
this.SelectedValue = () => string.Empty;
|
||||
this.SelectionUpdate(string.Empty);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
@inherits ConfigurationBase
|
||||
@typeparam T
|
||||
|
||||
<MudSelect T="T" Value="@this.SelectedValue()" Strict="@true" Disabled="@this.Disabled()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="@this.OptionChanged">
|
||||
@foreach (var data in this.Data)
|
||||
{
|
||||
<MudSelectItem Value="@data.Value">
|
||||
@data.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
+4
-1
@@ -1,6 +1,8 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration component for selecting a value from a list.
|
||||
@@ -30,6 +32,7 @@ public partial class ConfigurationSelect<T> : ConfigurationBase
|
||||
{
|
||||
this.SelectionUpdate(updatedValue);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
|
||||
private static string GetClass => $"{MARGIN_CLASS} rounded-lg";
|
||||
@@ -0,0 +1,8 @@
|
||||
@typeparam T
|
||||
@inherits ConfigurationBase
|
||||
|
||||
<MudField Label="@this.OptionDescription" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.Disabled()">
|
||||
<MudSlider T="@T" Size="Size.Medium" Value="@this.Value()" ValueChanged="@this.OptionChanged" Min="@this.Min" Max="@this.Max" Step="@this.Step" Immediate="@true" Disabled="@this.Disabled()">
|
||||
@this.Value() @this.Unit
|
||||
</MudSlider>
|
||||
</MudField>
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Numerics;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class ConfigurationSlider<T> : ConfigurationBase 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;
|
||||
|
||||
/// <summary>
|
||||
/// The value used for the slider.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<T> Value { get; set; } = () => T.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// An action which is called when the option is changed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Action<T> ValueUpdate { get; set; } = _ => { };
|
||||
|
||||
private async Task OptionChanged(T updatedValue)
|
||||
{
|
||||
this.ValueUpdate(updatedValue);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
@inherits ConfigurationBase
|
||||
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.OptionChanged"
|
||||
Label="@this.OptionDescription"
|
||||
Disabled="@this.Disabled()"
|
||||
Class="mb-3"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Lines="1"
|
||||
Immediate="@false"
|
||||
DebounceInterval="500"
|
||||
Variant="Variant.Outlined"
|
||||
/>
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class ConfigurationText : ConfigurationBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The text used for the textfield.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string> Text { get; set; } = () => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An action which is called when the option is changed.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Action<string> TextUpdate { get; set; } = _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// The icon to display next to the textfield.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Icon { get; set; } = Icons.Material.Filled.Info;
|
||||
|
||||
/// <summary>
|
||||
/// The color of the icon to use.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Color IconColor { get; set; } = Color.Default;
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
{
|
||||
this.TextUpdate(updatedText);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudExpansionPanel Class="border-solid border rounded-lg" IsInitiallyExpanded="@this.IsExpanded">
|
||||
<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"/>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public interface ITreeItem;
|
||||
@@ -1,3 +1,5 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="d-flex flex-column" style="@this.Height">
|
||||
<div class="flex-auto overflow-auto">
|
||||
@this.ChildContent
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using AIStudio.Components.Layout;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class InnerScrolling : ComponentBase
|
||||
public partial class InnerScrolling : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Set the height of anything above the scrolling content; usually a header.
|
||||
/// What we do is calc(100vh - THIS). Means, you can use multiple measures like
|
||||
/// What we do is calc(100vh - HeaderHeight). Means, you can use multiple measures like
|
||||
/// 230px - 3em. Default is 3em.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
@@ -21,5 +24,39 @@ public partial class InnerScrolling : ComponentBase
|
||||
[Parameter]
|
||||
public RenderFragment? FooterContent { get; set; }
|
||||
|
||||
private string Height => $"height: calc(100vh - {this.HeaderHeight});";
|
||||
[CascadingParameter]
|
||||
private MainLayout MainLayout { get; set; } = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.STATE_HAS_CHANGED ]);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.STATE_HAS_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@if (this.IssuesData.Any())
|
||||
{
|
||||
<MudPaper Class="pa-3 mr-3 mt-3 border-dashed border rounded-lg mud-alert-outlined-error">
|
||||
<MudText Typo="Typo.h6">Issues</MudText>
|
||||
<MudList T="string">
|
||||
@foreach (var issue in this.IssuesData)
|
||||
{
|
||||
<MudListItem Icon="@Icons.Material.Filled.Error" IconColor="Color.Error">
|
||||
@issue
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudPaper>
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class Issues : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public IEnumerable<string> IssuesData { get; set; } = [];
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<MudList Clickable="@this.Clickable" Class="@this.Classes">
|
||||
<MudList T="string" ReadOnly="@(!this.Clickable)" Class="@this.Classes">
|
||||
@foreach(var item in this.Items)
|
||||
{
|
||||
<MudListItem Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
|
||||
<MudListItem T="string" Icon="@this.Icon" Style="display: flex; align-items: flex-start;">
|
||||
<MudText Typo="Typo.body1" Style="text-align: justify; hyphens: auto;"><b>@item.Header:</b> @item.Text</MudText>
|
||||
</MudListItem>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<MudField Label="Read content from web?" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.AgentIsRunning">
|
||||
<MudSwitch T="bool" @bind-Value="@this.showWebContentReader" Color="Color.Primary" Disabled="@this.AgentIsRunning">
|
||||
@(this.showWebContentReader ? "Show web content options" : "Hide web content options")
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
|
||||
@if (this.showWebContentReader)
|
||||
{
|
||||
<MudField Label="Cleanup content by using a LLM agent?" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.AgentIsRunning">
|
||||
<MudSwitch T="bool" @bind-Value="@this.useContentCleanerAgent" Color="Color.Primary" Validation="@this.ValidateProvider" Disabled="@this.AgentIsRunning">
|
||||
@(this.useContentCleanerAgent ? "The content is cleaned using an LLM agent: the main content is extracted, advertisements and other irrelevant things are attempted to be removed; relative links are attempted to be converted into absolute links so that they can be used." : "No content cleaning")
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
<MudStack Row="@true" AlignItems="@AlignItems.Baseline" Class="mb-3">
|
||||
<MudTextField T="string" Label="URL from which to load the content" @bind-Value="@this.providedURL" Validation="@this.ValidateURL" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Link" Placeholder="https://..." HelperText="Loads the content from your URL. Does not work when the content is hidden behind a paywall." Variant="Variant.Outlined" Immediate="@true" Disabled="@this.AgentIsRunning"/>
|
||||
<MudButton Disabled="@(!this.IsReady || this.AgentIsRunning)" Variant="Variant.Filled" Size="Size.Large" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Download" OnClick="() => this.LoadFromWeb()">
|
||||
Fetch
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
@if (this.AgentIsRunning)
|
||||
{
|
||||
<div class="pa-1">
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class=""/>
|
||||
</div>
|
||||
<div class="pa-6">
|
||||
<MudSlider T="int" Disabled="@true" Value="@this.processStep" Min="@this.process.Min" Max="@this.process.Max" TickMarks="@true" Size="Size.Large" Variant="Variant.Filled" TickMarkLabels="@this.process.Labels" Class="mb-12"/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
@@ -0,0 +1,198 @@
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class ReadWebContent : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HTMLParser HTMLParser { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AgentTextContentCleaner AgentTextContentCleaner { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> ContentChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Settings.Provider ProviderSettings { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool AgentIsRunning { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<bool> AgentIsRunningChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Preselect { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool PreselectContentCleanerAgent { get; set; }
|
||||
|
||||
private Process<ReadWebContentSteps> process = Process<ReadWebContentSteps>.INSTANCE;
|
||||
private ProcessStepValue processStep;
|
||||
|
||||
private bool showWebContentReader;
|
||||
private bool useContentCleanerAgent;
|
||||
private string providedURL = string.Empty;
|
||||
private bool urlIsValid;
|
||||
private bool isProviderValid;
|
||||
|
||||
private Settings.Provider providerSettings;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if(this.Preselect)
|
||||
this.showWebContentReader = true;
|
||||
|
||||
if(this.PreselectContentCleanerAgent)
|
||||
this.useContentCleanerAgent = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.PreselectAgentTextContentCleanerOptions)
|
||||
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedAgentTextContentCleanerProvider);
|
||||
else
|
||||
this.providerSettings = this.ProviderSettings;
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.PreselectAgentTextContentCleanerOptions)
|
||||
this.providerSettings = this.ProviderSettings;
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task LoadFromWeb()
|
||||
{
|
||||
if(!this.IsReady)
|
||||
return;
|
||||
|
||||
var markdown = string.Empty;
|
||||
try
|
||||
{
|
||||
this.processStep = this.process[ReadWebContentSteps.LOADING];
|
||||
this.StateHasChanged();
|
||||
|
||||
var html = await this.HTMLParser.LoadWebContentHTML(new Uri(this.providedURL));
|
||||
|
||||
this.processStep = this.process[ReadWebContentSteps.PARSING];
|
||||
this.StateHasChanged();
|
||||
markdown = this.HTMLParser.ParseToMarkdown(html);
|
||||
|
||||
if (this.useContentCleanerAgent)
|
||||
{
|
||||
this.AgentTextContentCleaner.ProviderSettings = this.providerSettings;
|
||||
var additionalData = new Dictionary<string, string>
|
||||
{
|
||||
{ "sourceURL", this.providedURL },
|
||||
};
|
||||
|
||||
this.processStep = this.process[ReadWebContentSteps.CLEANING];
|
||||
this.AgentIsRunning = true;
|
||||
await this.AgentIsRunningChanged.InvokeAsync(this.AgentIsRunning);
|
||||
this.StateHasChanged();
|
||||
|
||||
var contentBlock = await this.AgentTextContentCleaner.ProcessInput(new ContentBlock
|
||||
{
|
||||
Time = DateTimeOffset.UtcNow,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.USER,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = markdown,
|
||||
},
|
||||
}, additionalData);
|
||||
|
||||
markdown = contentBlock.Content is ContentText text ? text.Text : markdown;
|
||||
|
||||
this.processStep = this.process[ReadWebContentSteps.DONE];
|
||||
this.AgentIsRunning = false;
|
||||
await this.AgentIsRunningChanged.InvokeAsync(this.AgentIsRunning);
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (this.AgentIsRunning)
|
||||
{
|
||||
this.processStep = this.process[ReadWebContentSteps.START];
|
||||
this.AgentIsRunning = false;
|
||||
await this.AgentIsRunningChanged.InvokeAsync(this.AgentIsRunning);
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
this.Content = markdown;
|
||||
await this.ContentChanged.InvokeAsync(this.Content);
|
||||
}
|
||||
|
||||
private bool IsReady
|
||||
{
|
||||
get
|
||||
{
|
||||
if(!this.urlIsValid)
|
||||
return false;
|
||||
|
||||
if(this.useContentCleanerAgent && !this.isProviderValid)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private string? ValidateProvider(bool shouldUseAgent)
|
||||
{
|
||||
if(shouldUseAgent && this.providerSettings == default)
|
||||
{
|
||||
this.isProviderValid = false;
|
||||
return "Please select a provider to use the cleanup agent.";
|
||||
}
|
||||
|
||||
this.isProviderValid = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidateURL(string url)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
this.urlIsValid = false;
|
||||
return "Please provide a URL to load the content from.";
|
||||
}
|
||||
|
||||
var urlParsingResult = Uri.TryCreate(url, UriKind.Absolute, out var uriResult);
|
||||
if(!urlParsingResult)
|
||||
{
|
||||
this.urlIsValid = false;
|
||||
return "Please provide a valid URL.";
|
||||
}
|
||||
|
||||
if(uriResult is not { Scheme: "http" or "https" })
|
||||
{
|
||||
this.urlIsValid = false;
|
||||
return "Please provide a valid HTTP or HTTPS URL.";
|
||||
}
|
||||
|
||||
this.urlIsValid = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public enum ReadWebContentSteps
|
||||
{
|
||||
START,
|
||||
LOADING,
|
||||
PARSING,
|
||||
CLEANING,
|
||||
DONE,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public readonly record struct TreeButton(WorkspaceBranch Branch, int Depth, string Text, string Icon, Func<Task> Action) : ITreeItem;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public readonly record struct TreeDivider : ITreeItem;
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public class TreeItemData : ITreeItem
|
||||
{
|
||||
public WorkspaceBranch Branch { get; init; } = WorkspaceBranch.NONE;
|
||||
|
||||
public int Depth { get; init; }
|
||||
|
||||
public string Text { get; init; } = string.Empty;
|
||||
|
||||
public string ShortenedText => Text.Length > 30 ? this.Text[..30] + "..." : this.Text;
|
||||
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
public TreeItemType Type { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public bool Expandable { get; init; } = true;
|
||||
|
||||
public IReadOnlyCollection<TreeItemData<ITreeItem>> Children { get; init; } = [];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public enum TreeItemType
|
||||
{
|
||||
NONE,
|
||||
|
||||
CHAT,
|
||||
WORKSPACE,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public enum WorkspaceBranch
|
||||
{
|
||||
NONE,
|
||||
|
||||
WORKSPACES,
|
||||
TEMPORARY_CHATS,
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<MudTreeView T="ITreeItem" Items="@this.treeItems" SelectionMode="SelectionMode.SingleSelection" Hover="@true" ExpandOnClick="@true">
|
||||
<ItemTemplate Context="item">
|
||||
@switch (item.Value)
|
||||
{
|
||||
case TreeDivider:
|
||||
<li style="min-height: 1em;">
|
||||
<MudDivider Style="margin-top: 1em; width: 90%; border-width: 3pt;"/>
|
||||
</li>
|
||||
break;
|
||||
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children" OnClick="() => this.LoadChat(treeItem.Path, true)">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@if (string.IsNullOrWhiteSpace(treeItem.Text))
|
||||
{
|
||||
@("Empty chat")
|
||||
}
|
||||
else
|
||||
{
|
||||
@treeItem.ShortenedText
|
||||
}
|
||||
</MudText>
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="Move to workspace" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.MoveChat(treeItem.Path)"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="Rename" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.RenameChat(treeItem.Path)"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="Delete" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.DeleteChat(treeItem.Path)"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
|
||||
<div style="justify-self: end;">
|
||||
<MudTooltip Text="Rename" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.RenameWorkspace(treeItem.Path)"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="Delete" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="() => this.DeleteWorkspace(treeItem.Path)"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" CanExpand="@treeItem.Expandable" Items="@treeItem.Children">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">@treeItem.Text</MudText>
|
||||
</div>
|
||||
</BodyContent>
|
||||
</MudTreeViewItem>
|
||||
}
|
||||
break;
|
||||
|
||||
case TreeButton treeButton:
|
||||
<li>
|
||||
<div class="mud-treeview-item-content" style="background-color: unset;">
|
||||
<div class="mud-treeview-item-arrow"></div>
|
||||
<MudButton StartIcon="@treeButton.Icon" Variant="Variant.Filled" OnClick="treeButton.Action">
|
||||
@treeButton.Text
|
||||
</MudButton>
|
||||
</div>
|
||||
</li>
|
||||
break;
|
||||
}
|
||||
</ItemTemplate>
|
||||
</MudTreeView>
|
||||
@@ -0,0 +1,533 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Components.CommonDialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components.Blocks;
|
||||
|
||||
public partial class Workspaces : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<ChatThread> CurrentChatThreadChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<Task> LoadedChatWasChanged { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
private const Placement WORKSPACE_ITEM_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
|
||||
private static readonly JsonSerializerOptions JSON_OPTIONS = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper),
|
||||
}
|
||||
};
|
||||
|
||||
private readonly List<TreeItemData<ITreeItem>> treeItems = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
//
|
||||
// Notice: In order to get the server-based loading to work, we need to respect the following rules:
|
||||
// - We must have initial tree items
|
||||
// - Those initial tree items cannot have children
|
||||
// - When assigning the tree items to the MudTreeViewItem component, we must set the Value property to the value of the item
|
||||
//
|
||||
|
||||
await this.LoadTreeItems();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task LoadTreeItems()
|
||||
{
|
||||
this.treeItems.Clear();
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Depth = 0,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = "Workspaces",
|
||||
Icon = Icons.Material.Filled.Folder,
|
||||
Expandable = true,
|
||||
Path = "root",
|
||||
Children = await this.LoadWorkspaces(),
|
||||
},
|
||||
});
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeDivider(),
|
||||
});
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Depth = 0,
|
||||
Branch = WorkspaceBranch.TEMPORARY_CHATS,
|
||||
Text = "Temporary chats",
|
||||
Icon = Icons.Material.Filled.Timer,
|
||||
Expandable = true,
|
||||
Path = "temp",
|
||||
Children = await this.LoadTemporaryChats(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadTemporaryChats()
|
||||
{
|
||||
var tempChildren = new List<TreeItemData<ITreeItem>>();
|
||||
|
||||
//
|
||||
// Search for workspace folders in the data directory:
|
||||
//
|
||||
|
||||
// Get the workspace root directory:
|
||||
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(temporaryDirectories);
|
||||
|
||||
// Enumerate the workspace directories:
|
||||
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
|
||||
{
|
||||
// Read the `name` file:
|
||||
var chatNamePath = Path.Join(tempChatDirPath, "name");
|
||||
var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
|
||||
|
||||
tempChildren.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.CHAT,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.TEMPORARY_CHATS,
|
||||
Text = chatName,
|
||||
Icon = Icons.Material.Filled.Timer,
|
||||
Expandable = false,
|
||||
Path = tempChatDirPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return tempChildren;
|
||||
}
|
||||
|
||||
public async Task<string> LoadWorkspaceName(Guid workspaceId)
|
||||
{
|
||||
if(workspaceId == Guid.Empty)
|
||||
return string.Empty;
|
||||
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
return await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaces()
|
||||
{
|
||||
var workspaces = new List<TreeItemData<ITreeItem>>();
|
||||
|
||||
//
|
||||
// Search for workspace folders in the data directory:
|
||||
//
|
||||
|
||||
// Get the workspace root directory:
|
||||
var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces");
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(workspaceDirectories);
|
||||
|
||||
// Enumerate the workspace directories:
|
||||
foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories))
|
||||
{
|
||||
// Read the `name` file:
|
||||
var workspaceNamePath = Path.Join(workspaceDirPath, "name");
|
||||
var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
|
||||
workspaces.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = true,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.WORKSPACE,
|
||||
Depth = 1,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = workspaceName,
|
||||
Icon = Icons.Material.Filled.Description,
|
||||
Expandable = true,
|
||||
Path = workspaceDirPath,
|
||||
Children = await this.LoadWorkspaceChats(workspaceDirPath),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
workspaces.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 1, "Add workspace",Icons.Material.Filled.LibraryAdd, this.AddWorkspace),
|
||||
});
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<TreeItemData<ITreeItem>>> LoadWorkspaceChats(string workspacePath)
|
||||
{
|
||||
var workspaceChats = new List<TreeItemData<ITreeItem>>();
|
||||
|
||||
// Enumerate the workspace directory:
|
||||
foreach (var chatPath in Directory.EnumerateDirectories(workspacePath))
|
||||
{
|
||||
// Read the `name` file:
|
||||
var chatNamePath = Path.Join(chatPath, "name");
|
||||
var chatName = await File.ReadAllTextAsync(chatNamePath, Encoding.UTF8);
|
||||
|
||||
workspaceChats.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeItemData
|
||||
{
|
||||
Type = TreeItemType.CHAT,
|
||||
Depth = 2,
|
||||
Branch = WorkspaceBranch.WORKSPACES,
|
||||
Text = chatName,
|
||||
Icon = Icons.Material.Filled.Chat,
|
||||
Expandable = false,
|
||||
Path = chatPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
workspaceChats.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
Expandable = false,
|
||||
Value = new TreeButton(WorkspaceBranch.WORKSPACES, 2, "Add chat",Icons.Material.Filled.AddComment, () => this.AddChat(workspacePath)),
|
||||
});
|
||||
|
||||
return workspaceChats;
|
||||
}
|
||||
|
||||
public async Task StoreChat(ChatThread chat)
|
||||
{
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
// Ensure the directory exists:
|
||||
Directory.CreateDirectory(chatDirectory);
|
||||
|
||||
// Save the chat name:
|
||||
var chatNamePath = Path.Join(chatDirectory, "name");
|
||||
await File.WriteAllTextAsync(chatNamePath, chat.Name);
|
||||
|
||||
// Save the thread as thread.json:
|
||||
var chatPath = Path.Join(chatDirectory, "thread.json");
|
||||
await File.WriteAllTextAsync(chatPath, JsonSerializer.Serialize(chat, JSON_OPTIONS), Encoding.UTF8);
|
||||
|
||||
// Reload the tree items:
|
||||
await this.LoadTreeItems();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<ChatThread?> LoadChat(string? chatPath, bool switchToChat)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(chatPath))
|
||||
return null;
|
||||
|
||||
if(!Directory.Exists(chatPath))
|
||||
return null;
|
||||
|
||||
// Check if the chat has unsaved changes:
|
||||
if (switchToChat && await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to load another chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Load Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, JSON_OPTIONS);
|
||||
if (switchToChat)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
}
|
||||
|
||||
return chat;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task DeleteChat(string? chatPath, bool askForConfirmation = true, bool unloadChat = true)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await this.LoadWorkspaceName(chat.WorkspaceId);
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{
|
||||
"Message", (chat.WorkspaceId == Guid.Empty) switch
|
||||
{
|
||||
true => $"Are you sure you want to delete the temporary chat '{chat.Name}'?",
|
||||
false => $"Are you sure you want to delete the chat '{chat.Name}' in the workspace '{workspaceName}'?",
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
string chatDirectory;
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString());
|
||||
else
|
||||
chatDirectory = Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString());
|
||||
|
||||
Directory.Delete(chatDirectory, true);
|
||||
await this.LoadTreeItems();
|
||||
|
||||
if(unloadChat && this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenameChat(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", $"Please enter a new or edit the name for your chat '{chat.Name}':" },
|
||||
{ "UserInput", chat.Name },
|
||||
{ "ConfirmText", "Rename" },
|
||||
{ "ConfirmColor", Color.Info },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
chat.Name = (dialogResult.Data as string)!;
|
||||
await this.StoreChat(chat);
|
||||
await this.LoadTreeItems();
|
||||
}
|
||||
|
||||
private async Task RenameWorkspace(string? workspacePath)
|
||||
{
|
||||
if(workspacePath is null)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var workspaceName = await this.LoadWorkspaceName(workspaceId);
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", $"Please enter a new or edit the name for your workspace '{workspaceName}':" },
|
||||
{ "UserInput", workspaceName },
|
||||
{ "ConfirmText", "Rename" },
|
||||
{ "ConfirmColor", Color.Info },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Rename Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var alteredWorkspaceName = (dialogResult.Data as string)!;
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, alteredWorkspaceName, Encoding.UTF8);
|
||||
await this.LoadTreeItems();
|
||||
}
|
||||
|
||||
private async Task AddWorkspace()
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Please name your workspace:" },
|
||||
{ "UserInput", string.Empty },
|
||||
{ "ConfirmText", "Add workspace" },
|
||||
{ "ConfirmColor", Color.Info },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>("Add Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.NewGuid();
|
||||
var workspacePath = Path.Join(SettingsManager.DataDirectory, "workspaces", workspaceId.ToString());
|
||||
Directory.CreateDirectory(workspacePath);
|
||||
|
||||
var workspaceNamePath = Path.Join(workspacePath, "name");
|
||||
await File.WriteAllTextAsync(workspaceNamePath, (dialogResult.Data as string)!, Encoding.UTF8);
|
||||
|
||||
await this.LoadTreeItems();
|
||||
}
|
||||
|
||||
private async Task DeleteWorkspace(string? workspacePath)
|
||||
{
|
||||
if(workspacePath is null)
|
||||
return;
|
||||
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var workspaceName = await this.LoadWorkspaceName(workspaceId);
|
||||
|
||||
// Determine how many chats are in the workspace:
|
||||
var chatCount = Directory.EnumerateDirectories(workspacePath).Count();
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", $"Are you sure you want to delete the workspace '{workspaceName}'? This will also delete {chatCount} chat(s) in this workspace." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
Directory.Delete(workspacePath, true);
|
||||
await this.LoadTreeItems();
|
||||
}
|
||||
|
||||
private async Task MoveChat(string? chatPath)
|
||||
{
|
||||
var chat = await this.LoadChat(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Please select the workspace where you want to move the chat to." },
|
||||
{ "SelectedWorkspace", chat.WorkspaceId },
|
||||
{ "ConfirmText", "Move chat" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = dialogResult.Data is Guid id ? id : default;
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Delete the chat from the current workspace or the temporary storage:
|
||||
if (chat.WorkspaceId == Guid.Empty)
|
||||
{
|
||||
// Case: The chat is stored in the temporary storage:
|
||||
await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case: The chat is stored in a workspace.
|
||||
await this.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", chat.WorkspaceId.ToString(), chat.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
|
||||
// Update the chat's workspace:
|
||||
chat.WorkspaceId = workspaceId;
|
||||
|
||||
// Handle the case where the chat is the active chat:
|
||||
if (this.CurrentChatThread?.ChatId == chat.ChatId)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await this.LoadedChatWasChanged();
|
||||
}
|
||||
|
||||
await this.StoreChat(chat);
|
||||
}
|
||||
|
||||
private async Task AddChat(string workspacePath)
|
||||
{
|
||||
// Check if the chat has unsaved changes:
|
||||
if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to create a another chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Create Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var chat = new ChatThread
|
||||
{
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Empty,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = "You are a helpful assistant!",
|
||||
Blocks = [],
|
||||
};
|
||||
|
||||
var chatPath = Path.Join(workspacePath, chat.ChatId.ToString());
|
||||
|
||||
await this.StoreChat(chat);
|
||||
await this.LoadChat(chatPath, switchToChat: true);
|
||||
await this.LoadTreeItems();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace AIStudio.Components.CommonDialogs;
|
||||
|
||||
public static class DialogOptions
|
||||
{
|
||||
public static readonly MudBlazor.DialogOptions FULLSCREEN = new()
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
FullWidth = true, MaxWidth = MaxWidth.Medium,
|
||||
};
|
||||
|
||||
public static readonly MudBlazor.DialogOptions FULLSCREEN_NO_HEADER = new()
|
||||
{
|
||||
NoHeader = true,
|
||||
CloseOnEscapeKey = true,
|
||||
FullWidth = true, MaxWidth = MaxWidth.Medium,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1">@this.Message</MudText>
|
||||
<MudTextField T="string" @bind-Text="@this.UserInput" Variant="Variant.Outlined" AutoGrow="@false" Lines="1" Label="Chat name" AutoFocus="@true" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="@this.ConfirmColor">@this.ConfirmText</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,43 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.CommonDialogs;
|
||||
|
||||
public partial class SingleInputDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string UserInput { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmText { get; set; } = "OK";
|
||||
|
||||
[Parameter]
|
||||
public Color ConfirmColor { get; set; } = Color.Error;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; set; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
#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 void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.UserInput));
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@using AIStudio.Tools
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.h4" Class="d-inline-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Large" Class="mr-3"/>
|
||||
Update from v@(META_DATA.Version) to v@(this.UpdateResponse.NewVersion)
|
||||
</MudText>
|
||||
<MudMarkdown Value="@this.UpdateResponse.Changelog" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Install later</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info">Install now</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.CommonDialogs;
|
||||
|
||||
/// <summary>
|
||||
/// The update dialog that is used to inform the user about an available update.
|
||||
/// </summary>
|
||||
public partial class UpdateDialog : ComponentBase
|
||||
{
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public UpdateResponse UpdateResponse { get; set; }
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1">@this.Message</MudText>
|
||||
<MudList T="Guid" @bind-SelectedValue="@this.selectedWorkspace">
|
||||
@foreach (var (workspaceName, workspaceId) in this.workspaces)
|
||||
{
|
||||
<MudListItem Text="@workspaceName" Icon="@Icons.Material.Filled.Description" Value="@workspaceId" />
|
||||
}
|
||||
</MudList>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
|
||||
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Info">@this.ConfirmText</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.CommonDialogs;
|
||||
|
||||
public partial class WorkspaceSelectionDialog : ComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private MudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Guid SelectedWorkspace { get; set; } = Guid.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ConfirmText { get; set; } = "OK";
|
||||
|
||||
private readonly Dictionary<string, Guid> workspaces = new();
|
||||
private Guid selectedWorkspace;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.selectedWorkspace = this.SelectedWorkspace;
|
||||
|
||||
// Get the workspace root directory:
|
||||
var workspaceDirectories = Path.Join(SettingsManager.DataDirectory, "workspaces");
|
||||
if(!Directory.Exists(workspaceDirectories))
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enumerate the workspace directories:
|
||||
foreach (var workspaceDirPath in Directory.EnumerateDirectories(workspaceDirectories))
|
||||
{
|
||||
// Read the `name` file:
|
||||
var workspaceNamePath = Path.Join(workspaceDirPath, "name");
|
||||
var workspaceName = await File.ReadAllTextAsync(workspaceNamePath, Encoding.UTF8);
|
||||
|
||||
// Add the workspace to the list:
|
||||
this.workspaces.Add(workspaceName, Guid.Parse(Path.GetFileName(workspaceDirPath)));
|
||||
}
|
||||
|
||||
this.StateHasChanged();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Confirm() => this.MudDialog.Close(DialogResult.Ok(this.selectedWorkspace));
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@@ -7,7 +8,7 @@ namespace AIStudio.Components;
|
||||
/// <summary>
|
||||
/// A base class for configuration options.
|
||||
/// </summary>
|
||||
public partial class ConfigurationBase : ComponentBase
|
||||
public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The description of the option, i.e., the name. Should be
|
||||
@@ -22,8 +23,66 @@ public partial class ConfigurationBase : ComponentBase
|
||||
[Parameter]
|
||||
public string OptionHelp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Is the option disabled?
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<bool> Disabled { get; set; } = () => false;
|
||||
|
||||
[Inject]
|
||||
public SettingsManager SettingsManager { get; init; } = null!;
|
||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
protected const string MARGIN_CLASS = "mb-6";
|
||||
protected static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the instance name input:
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected async Task InformAboutChange() => await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@inherits ConfigurationBase
|
||||
|
||||
<MudField Label="@this.OptionDescription" Variant="Variant.Outlined" HelperText="@this.OptionHelp" Class="@MARGIN_CLASS">
|
||||
<MudSwitch T="bool" Value="@this.State()" ValueChanged="@(updatedState => this.OptionChanged(updatedState))">
|
||||
@(this.State() ? this.LabelOn : this.LabelOff)
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
@@ -1,11 +0,0 @@
|
||||
@inherits ConfigurationBase
|
||||
@typeparam T
|
||||
|
||||
<MudSelect T="T" Value="@this.SelectedValue()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="selectedValue => this.OptionChanged(selectedValue)">
|
||||
@foreach (var data in this.Data)
|
||||
{
|
||||
<MudSelectItem Value="@data.Value">
|
||||
@data.Name
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@@ -1,24 +0,0 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A data structure to map a name to a value.
|
||||
/// </summary>
|
||||
/// <param name="Name">The name of the value, to be displayed in the UI.</param>
|
||||
/// <param name="Value">The value to be stored.</param>
|
||||
/// <typeparam name="T">The type of the value to store.</typeparam>
|
||||
public readonly record struct ConfigurationSelectData<T>(string Name, T Value);
|
||||
|
||||
/// <summary>
|
||||
/// A static factory class to get the lists of selectable values.
|
||||
/// </summary>
|
||||
public static class ConfigurationSelectDataFactory
|
||||
{
|
||||
public static IEnumerable<ConfigurationSelectData<SendBehavior>> GetSendBehaviorData()
|
||||
{
|
||||
yield return new("No key is sending the input", SendBehavior.NO_KEY_IS_SENDING);
|
||||
yield return new("Modifier key + enter is sending the input", SendBehavior.MODIFER_ENTER_IS_SENDING);
|
||||
yield return new("Enter is sending the input", SendBehavior.ENTER_IS_SENDING);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@inherits ConfigurationBase
|
||||
|
||||
<MudField Label="@this.OptionDescription" Variant="Variant.Outlined" HelperText="@this.OptionHelp" Class="@MARGIN_CLASS">
|
||||
<MudButton Variant="Variant.Filled" StartIcon="@this.TriggerIcon" OnClick="@this.Click">
|
||||
@this.TriggerText
|
||||
</MudButton>
|
||||
</MudField>
|
||||
@@ -1,24 +0,0 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class ConfigurationTrigger : ConfigurationBase
|
||||
{
|
||||
[Parameter]
|
||||
public string TriggerText { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string TriggerIcon { get; set; } = Icons.Material.Filled.AddBox;
|
||||
|
||||
[Parameter]
|
||||
public Action OnClickSync { get; set; } = () => { };
|
||||
|
||||
[Parameter]
|
||||
public Func<Task> OnClickAsync { get; set; } = () => Task.CompletedTask;
|
||||
|
||||
private async Task Click()
|
||||
{
|
||||
this.OnClickSync();
|
||||
await this.OnClickAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,57 @@
|
||||
@inherits LayoutComponentBase
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudPaper Height="calc(100vh);" Elevation="0">
|
||||
<MudLayout>
|
||||
<MudDrawerContainer Class="mud-height-full absolute">
|
||||
<MudDrawer Elevation="0" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@true" Color="Color.Default">
|
||||
<MudNavMenu>
|
||||
<MudTooltip Text="Home" Placement="Placement.Right">
|
||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Chats" Placement="Placement.Right">
|
||||
<MudNavLink Href="/chat" Icon="@Icons.Material.Filled.Chat">Chats</MudNavLink>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Supporters" Placement="Placement.Right">
|
||||
<MudNavLink Href="/supporters" Icon="@Icons.Material.Filled.Favorite" IconColor="Color.Error">Supporters</MudNavLink>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="About" Placement="Placement.Right">
|
||||
<MudNavLink Href="/about" Icon="@Icons.Material.Filled.Info">About</MudNavLink>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Settings" Placement="Placement.Right">
|
||||
<MudNavLink Href="/settings" Icon="@Icons.Material.Filled.Settings">Settings</MudNavLink>
|
||||
</MudTooltip>
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
</MudDrawerContainer>
|
||||
@if (!this.performingUpdate)
|
||||
{
|
||||
<MudDrawerContainer Class="mud-height-full absolute">
|
||||
<MudDrawer @bind-Open="@this.navBarOpen" MiniWidth="@NAVBAR_COLLAPSED_WIDTH" Width="@NAVBAR_EXPANDED_WIDTH" Elevation="1" Fixed="@true" Variant="@DrawerVariant.Mini" OpenMiniOnHover="@(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.EXPAND_ON_HOVER)" Color="Color.Default">
|
||||
<MudNavMenu>
|
||||
@foreach (var navBarItem in NAV_ITEMS)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.NEVER_EXPAND_USE_TOOLTIPS)
|
||||
{
|
||||
<MudTooltip Text="@navBarItem.Name" Placement="Placement.Right">
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" IconColor="@navBarItem.IconColor">@navBarItem.Name</MudNavLink>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" IconColor="@navBarItem.IconColor">@navBarItem.Name</MudNavLink>
|
||||
}
|
||||
}
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
</MudDrawerContainer>
|
||||
}
|
||||
|
||||
<MudMainContent Class="mud-height-full pt-1">
|
||||
<MudMainContent Class="mud-height-full pt-1" Style="@this.PaddingLeft">
|
||||
<MudContainer Fixed="@true" Class="mud-height-full" Style="margin-left: 5em; width: calc(100% - 5em);">
|
||||
@this.Body
|
||||
@if (!this.performingUpdate && this.IsUpdateAlertVisible)
|
||||
{
|
||||
<MudAlert NoIcon="@true" Severity="Severity.Info" Variant="Variant.Filled" ShowCloseIcon="@true" Dense="@true" CloseIconClicked="() => this.DismissUpdate()" Class="mt-2 mb-2">
|
||||
<div class="d-inline-flex align-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Medium" Class="mr-3"/>
|
||||
An update to version @this.updateToVersion is available.
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Dark" Size="Size.Small" Class="ml-3" OnClick="() => this.ShowUpdateDialog()">
|
||||
Show details
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (!this.performingUpdate)
|
||||
{
|
||||
<CascadingValue Value="@this" IsFixed="true">
|
||||
@this.Body
|
||||
</CascadingValue>
|
||||
}
|
||||
|
||||
<MudOverlay Visible="@this.performingUpdate" DarkBackground="@true" LockScroll="@true">
|
||||
<MudText Typo="Typo.h3">Please wait for the update to complete...</MudText>
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="@true" Size="Size.Large" Rounded="@true"/>
|
||||
</MudOverlay>
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@@ -1,17 +1,70 @@
|
||||
using AIStudio.Components.CommonDialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Routing;
|
||||
|
||||
using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components.Layout;
|
||||
|
||||
public partial class MainLayout : LayoutComponentBase
|
||||
public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; set; } = null!;
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private Rust Rust { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager NavigationManager { get; init; } = null!;
|
||||
|
||||
public string AdditionalHeight { get; private set; } = "0em";
|
||||
|
||||
private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
|
||||
|
||||
private const int NAVBAR_COLLAPSED_WIDTH_INT = 4;
|
||||
private const int NAVBAR_EXPANDED_WIDTH_INT = 10;
|
||||
private static readonly string NAVBAR_COLLAPSED_WIDTH = $"{NAVBAR_COLLAPSED_WIDTH_INT}em";
|
||||
private static readonly string NAVBAR_EXPANDED_WIDTH = $"{NAVBAR_EXPANDED_WIDTH_INT}em";
|
||||
|
||||
private bool navBarOpen;
|
||||
private bool isUpdateAvailable;
|
||||
private bool performingUpdate;
|
||||
private bool userDismissedUpdate;
|
||||
private string updateToVersion = string.Empty;
|
||||
private UpdateResponse? currentUpdateResponse;
|
||||
|
||||
private static readonly IReadOnlyCollection<NavBarItem> NAV_ITEMS = new List<NavBarItem>
|
||||
{
|
||||
new("Home", Icons.Material.Filled.Home, Color.Default, "/", true),
|
||||
new("Chat", Icons.Material.Filled.Chat, Color.Default, "/chat", false),
|
||||
new("Assistants", Icons.Material.Filled.Apps, Color.Default ,"/assistants", false),
|
||||
new("Supporters", Icons.Material.Filled.Favorite, Color.Error ,"/supporters", false),
|
||||
new("About", Icons.Material.Filled.Info, Color.Default ,"/about", false),
|
||||
new("Settings", Icons.Material.Filled.Settings, Color.Default ,"/settings", false),
|
||||
};
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.NavigationManager.RegisterLocationChangingHandler(this.OnLocationChanging);
|
||||
|
||||
//
|
||||
// We use the Tauri API (Rust) to get the data and config directories
|
||||
// for this app.
|
||||
@@ -21,10 +74,151 @@ public partial class MainLayout : LayoutComponentBase
|
||||
|
||||
// Store the directories in the settings manager:
|
||||
SettingsManager.ConfigDirectory = configDir;
|
||||
SettingsManager.DataDirectory = dataDir;
|
||||
SettingsManager.DataDirectory = Path.Join(dataDir, "data");
|
||||
Directory.CreateDirectory(SettingsManager.DataDirectory);
|
||||
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
// Set the js runtime for the update service:
|
||||
UpdateService.SetBlazorDependencies(this.JsRuntime, this.Snackbar);
|
||||
TemporaryChatService.Initialize();
|
||||
|
||||
// Should the navigation bar be open by default?
|
||||
if(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||
this.navBarOpen = true;
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.USER_SEARCH_FOR_UPDATE:
|
||||
this.userDismissedUpdate = false;
|
||||
break;
|
||||
|
||||
case Event.UPDATE_AVAILABLE:
|
||||
if (data is UpdateResponse updateResponse)
|
||||
{
|
||||
this.currentUpdateResponse = updateResponse;
|
||||
this.isUpdateAvailable = updateResponse.UpdateIsAvailable;
|
||||
this.updateToVersion = updateResponse.NewVersion;
|
||||
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
await this.SendMessage<bool>(Event.STATE_HAS_CHANGED);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
if(this.SettingsManager.ConfigurationData.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
||||
this.navBarOpen = true;
|
||||
else
|
||||
this.navBarOpen = false;
|
||||
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task DismissUpdate()
|
||||
{
|
||||
this.userDismissedUpdate = true;
|
||||
this.AdditionalHeight = "0em";
|
||||
|
||||
await this.SendMessage<bool>(Event.STATE_HAS_CHANGED);
|
||||
}
|
||||
|
||||
private bool IsUpdateAlertVisible
|
||||
{
|
||||
get
|
||||
{
|
||||
var state = this.isUpdateAvailable && !this.userDismissedUpdate;
|
||||
this.AdditionalHeight = state ? "3em" : "0em";
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowUpdateDialog()
|
||||
{
|
||||
if(this.currentUpdateResponse is null)
|
||||
return;
|
||||
|
||||
//
|
||||
// Replace the fir line with `# Changelog`:
|
||||
//
|
||||
var changelog = this.currentUpdateResponse.Value.Changelog;
|
||||
if (!string.IsNullOrWhiteSpace(changelog))
|
||||
{
|
||||
var lines = changelog.Split('\n');
|
||||
if (lines.Length > 0)
|
||||
lines[0] = "# Changelog";
|
||||
|
||||
changelog = string.Join('\n', lines);
|
||||
}
|
||||
|
||||
var updatedResponse = this.currentUpdateResponse.Value with { Changelog = changelog };
|
||||
var dialogParameters = new DialogParameters<UpdateDialog>
|
||||
{
|
||||
{ x => x.UpdateResponse, updatedResponse }
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<UpdateDialog>("Update", dialogParameters, DialogOptions.FULLSCREEN_NO_HEADER);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
this.performingUpdate = true;
|
||||
this.StateHasChanged();
|
||||
await this.Rust.InstallUpdate(this.JsRuntime);
|
||||
}
|
||||
|
||||
private async ValueTask OnLocationChanging(LocationChangingContext context)
|
||||
{
|
||||
if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to leave the chat page? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Leave Chat Page", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
{
|
||||
context.PreventNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
// User accepted to leave the chat page, reset the chat state:
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.RESET_CHAT_STATE);
|
||||
}
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace AIStudio.Components.Layout;
|
||||
|
||||
public record NavBarItem(string Name, string Icon, Color IconColor, string Path, bool MatchAll);
|
||||
@@ -0,0 +1,53 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
|
||||
{
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
base.OnInitialized();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public abstract Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
|
||||
|
||||
public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected async Task SendMessage<T>(Event triggeredEvent, T? data = default)
|
||||
{
|
||||
await this.MessageBus.SendMessage(this, triggeredEvent, data);
|
||||
}
|
||||
|
||||
protected async Task<TResult?> SendMessageWithResult<TPayload, TResult>(Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return await this.MessageBus.SendMessageUseFirstResult<TPayload, TResult>(this, triggeredEvent, data);
|
||||
}
|
||||
|
||||
protected void ApplyFilters(ComponentBase[] components, Event[] events)
|
||||
{
|
||||
this.MessageBus.ApplyFilters(this, components, events);
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,18 @@
|
||||
<MudText>
|
||||
The following list shows the versions of the MindWork AI Studio, the used compilers, build time, etc.:
|
||||
</MudText>
|
||||
<MudList Clickable="@true">
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Chat" Text="@VersionApp"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Timer" Text="@BuildTime"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Build" Text="@VersionDotnetSdk"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Memory" Text="@VersionDotnetRuntime"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Build" Text="@VersionRust"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
|
||||
<MudListItem Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
|
||||
<MudList T="string" Class="mb-3">
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Chat" Text="@VersionApp"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Timer" Text="@BuildTime"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@VersionDotnetSdk"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@VersionDotnetRuntime"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@VersionRust"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
|
||||
</MudList>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Update" OnClick="() => this.CheckForUpdate()">
|
||||
Check for updates
|
||||
</MudButton>
|
||||
</ExpansionPanel>
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="Changelog">
|
||||
@@ -44,6 +47,8 @@
|
||||
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/1Password/arboard" RepositoryUrl="https://github.com/1Password/arboard" UseCase="To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system."/>
|
||||
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor."/>
|
||||
<ThirdPartyComponent Name="flexi_logger" Developer="emabee & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/emabee/flexi_logger" RepositoryUrl="https://github.com/emabee/flexi_logger" UseCase="This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."/>
|
||||
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
|
||||
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>
|
||||
</MudGrid>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Reflection;
|
||||
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Pages;
|
||||
|
||||
public partial class About : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
|
||||
@@ -135,4 +140,9 @@ public partial class About : ComponentBase
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
""";
|
||||
|
||||
private async Task CheckForUpdate()
|
||||
{
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
@page "/assistants"
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
Assistants
|
||||
</MudText>
|
||||
|
||||
<MudStack Row="@true" Wrap="@Wrap.Wrap">
|
||||
<AssistantBlock Name="Icon Finder" Description="Using a LLM to find an icon for a given context." Icon="@Icons.Material.Filled.FindInPage" Link="/assistant/icons"/>
|
||||
<AssistantBlock Name="Text Summarizer" Description="Using a LLM to summarize a given text." Icon="@Icons.Material.Filled.TextSnippet" Link="/assistant/summarizer"/>
|
||||
<AssistantBlock Name="Translation" Description="Translate text into another language." Icon="@Icons.Material.Filled.Translate" Link="/assistant/translation"/>
|
||||
<AssistantBlock Name="Coding" Description="Get coding and debugging support from a LLM." Icon="@Icons.Material.Filled.Code" Link="/assistant/coding"/>
|
||||
</MudStack>
|
||||
@@ -0,0 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Pages;
|
||||
|
||||
public partial class Assistants : ComponentBase;
|
||||
@@ -1,9 +1,22 @@
|
||||
@page "/chat"
|
||||
@using AIStudio.Chat
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Settings.DataModel
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-2">Chats</MudText>
|
||||
<MudSelect T="Provider" @bind-Value="@this.selectedProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined">
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
@if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
|
||||
{
|
||||
@($"Chat in Workspace \"{this.currentWorkspaceName}\"")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Temporary Chat")
|
||||
}
|
||||
</MudText>
|
||||
|
||||
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-2 rounded-lg" Variant="Variant.Outlined">
|
||||
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
{
|
||||
<MudSelectItem Value="@provider"/>
|
||||
@@ -24,5 +37,64 @@
|
||||
<MudPaper Style="flex: 0 0 auto;">
|
||||
<MudTextField T="string" @ref="@this.inputField" @bind-Text="@this.userInput" Variant="Variant.Outlined" AutoGrow="@true" Lines="3" MaxLines="12" Label="@this.InputLabel" Placeholder="@this.ProviderPlaceholder" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Send" OnAdornmentClick="() => this.SendMessage()" ReadOnly="!this.IsProviderSelected || this.isStreaming" Immediate="@true" OnKeyUp="this.InputKeyEvent" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
</MudPaper>
|
||||
<MudPaper Class="mt-1" Outlined="@true">
|
||||
<MudToolBar WrapContent="true">
|
||||
@if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="Your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaces()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
|
||||
</MudTooltip>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
|
||||
{
|
||||
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudToolBar>
|
||||
</MudPaper>
|
||||
</FooterContent>
|
||||
</InnerScrolling>
|
||||
</InnerScrolling>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior != WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudDrawer @bind-Open="@this.workspacesVisible" Width="40em" Height="100%" Anchor="Anchor.Start" Variant="DrawerVariant.Temporary" Elevation="1">
|
||||
<MudDrawerHeader>
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
<MudText Typo="Typo.h6" Class="mr-3">
|
||||
Your workspaces
|
||||
</MudText>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Variant="Variant.Filled" Color="Color.Default" Size="Size.Small" OnClick="() => this.ToggleWorkspaces()"/>
|
||||
</MudStack>
|
||||
</MudDrawerHeader>
|
||||
<MudDrawerContainer Class="ml-6">
|
||||
<Workspaces @ref="this.workspaces" @bind-CurrentChatThread="@this.chatThread" LoadedChatWasChanged="this.LoadedChatChanged"/>
|
||||
</MudDrawerContainer>
|
||||
</MudDrawer>
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Components.Blocks;
|
||||
using AIStudio.Components.CommonDialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Components.Pages;
|
||||
|
||||
/// <summary>
|
||||
/// The chat page.
|
||||
/// </summary>
|
||||
public partial class Chat : ComponentBase
|
||||
public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; set; } = null!;
|
||||
@@ -19,14 +25,23 @@ public partial class Chat : ComponentBase
|
||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public Random RNG { get; set; } = null!;
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IDialogService DialogService { get; set; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
private AIStudio.Settings.Provider selectedProvider;
|
||||
private AIStudio.Settings.Provider providerSettings;
|
||||
private ChatThread? chatThread;
|
||||
private bool hasUnsavedChanges;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private bool workspacesVisible;
|
||||
private Workspaces? workspaces;
|
||||
|
||||
// Unfortunately, we need the input field reference to clear it after sending a message.
|
||||
// This is necessary because we have to handle the key events ourselves. Otherwise,
|
||||
@@ -37,47 +52,81 @@ public partial class Chat : ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Ensure that the settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.PreselectChatOptions)
|
||||
{
|
||||
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.PreselectedChatProvider);
|
||||
}
|
||||
|
||||
// For now, we just create a new chat thread.
|
||||
// Later we want the chats to be persisted
|
||||
// across page loads and organize them in
|
||||
// a chat history & workspaces.
|
||||
this.chatThread = new("Thread 1", this.RNG.Next(), "You are a helpful assistant!", []);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.selectedProvider.UsedProvider != Providers.NONE;
|
||||
private bool IsProviderSelected => this.providerSettings.UsedProvider != Providers.NONE;
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
|
||||
|
||||
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.selectedProvider.InstanceName}', provider '{this.selectedProvider.UsedProvider.ToName()}')" : "Select a provider first";
|
||||
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.providerSettings.InstanceName}', provider '{this.providerSettings.UsedProvider.ToName()}')" : "Select a provider first";
|
||||
|
||||
private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0;
|
||||
|
||||
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
|
||||
|
||||
private async Task SendMessage()
|
||||
{
|
||||
if (!this.IsProviderSelected)
|
||||
return;
|
||||
|
||||
// Create a new chat thread if necessary:
|
||||
var threadName = this.ExtractThreadName(this.userInput);
|
||||
|
||||
if (this.chatThread is null)
|
||||
{
|
||||
this.chatThread = new()
|
||||
{
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = threadName,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = "You are a helpful assistant!",
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.chatThread.Name))
|
||||
this.chatThread.Name = threadName;
|
||||
}
|
||||
|
||||
//
|
||||
// Add the user message to the thread:
|
||||
//
|
||||
var time = DateTimeOffset.Now;
|
||||
this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, new ContentText
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
// Text content properties:
|
||||
Text = this.userInput,
|
||||
})
|
||||
{
|
||||
// Content block properties:
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.USER,
|
||||
Content = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
},
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
//
|
||||
// Add the AI response to the thread:
|
||||
//
|
||||
@@ -88,9 +137,12 @@ public partial class Chat : ComponentBase
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(new ContentBlock(time, ContentType.TEXT, aiText)
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
});
|
||||
|
||||
// Clear the input field:
|
||||
@@ -99,13 +151,21 @@ public partial class Chat : ComponentBase
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = 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.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
@@ -113,6 +173,7 @@ public partial class Chat : ComponentBase
|
||||
|
||||
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
|
||||
{
|
||||
this.hasUnsavedChanges = true;
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
@@ -135,4 +196,216 @@ public partial class Chat : ComponentBase
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleWorkspaces()
|
||||
{
|
||||
this.workspacesVisible = !this.workspacesVisible;
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
{
|
||||
if(this.workspaces is null)
|
||||
return;
|
||||
|
||||
if(this.chatThread is null)
|
||||
return;
|
||||
|
||||
if (!this.CanThreadBeSaved)
|
||||
return;
|
||||
|
||||
await this.workspaces.StoreChat(this.chatThread);
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
private string ExtractThreadName(string firstUserInput)
|
||||
{
|
||||
// We select the first 10 words of the user input:
|
||||
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var threadName = string.Join(' ', words.Take(10));
|
||||
|
||||
// If the thread name is empty, we use a default name:
|
||||
if (string.IsNullOrWhiteSpace(threadName))
|
||||
threadName = "Thread";
|
||||
|
||||
return threadName;
|
||||
}
|
||||
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
|
||||
{
|
||||
if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chatThread is not null && this.workspaces is not null && deletePreviousChat)
|
||||
{
|
||||
string chatPath;
|
||||
if (this.chatThread.WorkspaceId == Guid.Empty)
|
||||
{
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString());
|
||||
}
|
||||
|
||||
await this.workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
|
||||
}
|
||||
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
|
||||
if (!useSameWorkspace)
|
||||
{
|
||||
this.chatThread = null;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.chatThread = new()
|
||||
{
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = string.Empty,
|
||||
Seed = this.RNG.Next(),
|
||||
SystemPrompt = "You are a helpful assistant!",
|
||||
Blocks = [],
|
||||
};
|
||||
}
|
||||
|
||||
await this.inputField.Clear();
|
||||
}
|
||||
|
||||
private async Task MoveChatToWorkspace()
|
||||
{
|
||||
if(this.chatThread is null)
|
||||
return;
|
||||
|
||||
if(this.workspaces is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
|
||||
};
|
||||
|
||||
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
|
||||
var confirmationDialogResult = await confirmationDialogReference.Result;
|
||||
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", "Please select the workspace where you want to move the chat to." },
|
||||
{ "SelectedWorkspace", this.chatThread?.WorkspaceId },
|
||||
{ "ConfirmText", "Move chat" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var workspaceId = dialogResult.Data is Guid id ? id : default;
|
||||
if (workspaceId == Guid.Empty)
|
||||
return;
|
||||
|
||||
// Delete the chat from the current workspace or the temporary storage:
|
||||
if (this.chatThread!.WorkspaceId == Guid.Empty)
|
||||
{
|
||||
// Case: The chat is stored in the temporary storage:
|
||||
await this.workspaces.DeleteChat(Path.Join(SettingsManager.DataDirectory, "tempChats", this.chatThread.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Case: The chat is stored in a workspace.
|
||||
await this.workspaces.DeleteChat(Path.Join(SettingsManager.DataDirectory, "workspaces", this.chatThread.WorkspaceId.ToString(), this.chatThread.ChatId.ToString()), askForConfirmation: false, unloadChat: false);
|
||||
}
|
||||
|
||||
this.chatThread!.WorkspaceId = workspaceId;
|
||||
await this.SaveThread();
|
||||
|
||||
this.currentWorkspaceId = this.chatThread.WorkspaceId;
|
||||
this.currentWorkspaceName = await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
{
|
||||
if(this.workspaces is null)
|
||||
return;
|
||||
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = this.chatThread?.WorkspaceId ?? Guid.Empty;
|
||||
this.currentWorkspaceName = this.chatThread is null ? string.Empty : await this.workspaces.LoadWorkspaceName(this.chatThread.WorkspaceId);
|
||||
|
||||
await this.inputField.Clear();
|
||||
}
|
||||
|
||||
private void ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.currentWorkspaceId = Guid.Empty;
|
||||
this.currentWorkspaceName = string.Empty;
|
||||
this.chatThread = null;
|
||||
}
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.RESET_CHAT_STATE:
|
||||
this.ResetState();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IAsyncDisposable
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if(this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@page "/assistant/coding"
|
||||
@using AIStudio.Settings
|
||||
@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">
|
||||
<CodingContextItem @bind-CodingContext="@codingContext"/>
|
||||
</ExpansionPanel>
|
||||
}
|
||||
</MudExpansionPanels>
|
||||
<MudButton Variant="Variant.Filled" OnClick="() => this.AddCodingContext()" Class="mb-3">
|
||||
Add context
|
||||
</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>
|
||||
@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"/>
|
||||
}
|
||||
</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>
|
||||
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" OnClick="() => this.GetSupport()" Class="mb-3">
|
||||
Get support
|
||||
</MudButton>
|
||||
@@ -0,0 +1,123 @@
|
||||
using System.Text;
|
||||
|
||||
namespace AIStudio.Components.Pages.Coding;
|
||||
|
||||
public partial class AssistantCoding : AssistantBaseCore
|
||||
{
|
||||
protected override string Title => "Coding Assistant";
|
||||
|
||||
protected override string Description =>
|
||||
"""
|
||||
This coding assistant supports you in writing code. Provide some coding context by copying and pasting
|
||||
your code into the input fields. You might assign an ID to your code snippet to easily reference it later.
|
||||
When you have compiler messages, you can paste them into the input fields to get help with debugging as well.
|
||||
""";
|
||||
|
||||
protected override string SystemPrompt =>
|
||||
"""
|
||||
You are a friendly, helpful senior software developer with extensive experience in various programming languages
|
||||
and concepts. You are familiar with principles like DRY, KISS, YAGNI, and SOLID and can apply and explain them.
|
||||
You know object-oriented programming, as well as functional programming and procedural programming. You are also
|
||||
familiar with design patterns and can explain them. You are an expert of debugging and can help with compiler
|
||||
messages. You can also help with code refactoring and optimization.
|
||||
|
||||
When the user asks in a different language than English, you answer in the same language!
|
||||
""";
|
||||
|
||||
private readonly List<CodingContext> codingContexts = new();
|
||||
private bool provideCompilerMessages;
|
||||
private string compilerMessages = string.Empty;
|
||||
private string questions = string.Empty;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string? ValidatingCompilerMessages(string checkCompilerMessages)
|
||||
{
|
||||
if(!this.provideCompilerMessages)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(checkCompilerMessages))
|
||||
return "Please provide the compiler messages.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidateQuestions(string checkQuestions)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(checkQuestions))
|
||||
return "Please provide your questions.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void AddCodingContext()
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
private async Task GetSupport()
|
||||
{
|
||||
await this.form!.Validate();
|
||||
if (!this.inputIsValid)
|
||||
return;
|
||||
|
||||
var sbContext = new StringBuilder();
|
||||
if (this.codingContexts.Count > 0)
|
||||
{
|
||||
sbContext.AppendLine("I have the following coding context:");
|
||||
sbContext.AppendLine();
|
||||
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:");
|
||||
sbContext.AppendLine("```");
|
||||
sbContext.AppendLine(codingContext.Code);
|
||||
sbContext.AppendLine("```");
|
||||
sbContext.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
var sbCompilerMessages = new StringBuilder();
|
||||
if (this.provideCompilerMessages)
|
||||
{
|
||||
sbCompilerMessages.AppendLine("I have the following compiler messages:");
|
||||
sbCompilerMessages.AppendLine();
|
||||
sbCompilerMessages.AppendLine("```");
|
||||
sbCompilerMessages.AppendLine(this.compilerMessages);
|
||||
sbCompilerMessages.AppendLine("```");
|
||||
sbCompilerMessages.AppendLine();
|
||||
}
|
||||
|
||||
this.CreateChatThread();
|
||||
var time = this.AddUserRequest(
|
||||
$"""
|
||||
{sbContext}
|
||||
{sbCompilerMessages}
|
||||
|
||||
My questions are:
|
||||
{this.questions}
|
||||
""");
|
||||
|
||||
await this.AddAIResponseAsync(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Components.Pages.Coding;
|
||||
|
||||
public sealed class CodingContext(string id, CommonCodingLanguages language, string otherLanguage, string code)
|
||||
{
|
||||
public CodingContext() : this(string.Empty, CommonCodingLanguages.NONE, string.Empty, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public string Id { get; set; } = id;
|
||||
|
||||
public CommonCodingLanguages Language { get; set; } = language;
|
||||
|
||||
public string OtherLanguage { get; set; } = otherLanguage;
|
||||
|
||||
public string Code { get; set; } = code;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
|
||||
{
|
||||
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.CodingContext.Language is CommonCodingLanguages.OTHER)
|
||||
{
|
||||
<MudTextField T="string" @bind-Text="@this.CodingContext.OtherLanguage" Validation="@this.ValidatingOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
}
|
||||
</MudStack>
|
||||
<MudTextField T="string" @bind-Text="@this.CodingContext.Code" Validation="@this.ValidatingCode" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your code" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES" />
|
||||
@@ -0,0 +1,50 @@
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components.Pages.Coding;
|
||||
|
||||
public partial class CodingContextItem : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public CodingContext CodingContext { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<CodingContext> CodingContextChanged { get; set; }
|
||||
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; set; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
#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();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string? ValidatingCode(string code)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(code))
|
||||
return $"{this.CodingContext.Id}: Please provide your input.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidatingOtherLanguage(string language)
|
||||
{
|
||||
if(this.CodingContext.Language != CommonCodingLanguages.OTHER)
|
||||
return null;
|
||||
|
||||
if(string.IsNullOrWhiteSpace(language))
|
||||
return "Please specify the language.";
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace AIStudio.Components.Pages.Coding;
|
||||
|
||||
public static class CommonCodingLanguageExtensions
|
||||
{
|
||||
public static string Name(this CommonCodingLanguages language) => language switch
|
||||
{
|
||||
CommonCodingLanguages.NONE => "None",
|
||||
|
||||
CommonCodingLanguages.BASH => "Bash",
|
||||
CommonCodingLanguages.BLAZOR => ".NET Blazor",
|
||||
CommonCodingLanguages.C => "C",
|
||||
CommonCodingLanguages.CPP => "C++",
|
||||
CommonCodingLanguages.CSHARP => "C#",
|
||||
CommonCodingLanguages.CSS => "CSS",
|
||||
CommonCodingLanguages.FORTRAN => "Fortran",
|
||||
CommonCodingLanguages.GDSCRIPT => "GDScript",
|
||||
CommonCodingLanguages.GO => "Go",
|
||||
CommonCodingLanguages.HTML => "HTML",
|
||||
CommonCodingLanguages.JAVA => "Java",
|
||||
CommonCodingLanguages.JAVASCRIPT => "JavaScript",
|
||||
CommonCodingLanguages.JSON => "JSON",
|
||||
CommonCodingLanguages.JULIA => "Julia",
|
||||
CommonCodingLanguages.KOTLIN => "Kotlin",
|
||||
CommonCodingLanguages.LUA => "Lua",
|
||||
CommonCodingLanguages.MARKDOWN => "Markdown",
|
||||
CommonCodingLanguages.MATHEMATICA => "Mathematica",
|
||||
CommonCodingLanguages.MATLAB => "MATLAB",
|
||||
CommonCodingLanguages.PHP => "PHP",
|
||||
CommonCodingLanguages.POWERSHELL => "PowerShell",
|
||||
CommonCodingLanguages.PROLOG => "Prolog",
|
||||
CommonCodingLanguages.PYTHON => "Python",
|
||||
CommonCodingLanguages.R => "R",
|
||||
CommonCodingLanguages.RUBY => "Ruby",
|
||||
CommonCodingLanguages.RUST => "Rust",
|
||||
CommonCodingLanguages.SQL => "SQL",
|
||||
CommonCodingLanguages.SWIFT => "Swift",
|
||||
CommonCodingLanguages.TYPESCRIPT => "TypeScript",
|
||||
CommonCodingLanguages.XML => "XML",
|
||||
|
||||
CommonCodingLanguages.OTHER => "Other",
|
||||
_ => "Unknown"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace AIStudio.Components.Pages.Coding;
|
||||
|
||||
public enum CommonCodingLanguages
|
||||
{
|
||||
NONE,
|
||||
|
||||
BASH,
|
||||
BLAZOR,
|
||||
C,
|
||||
CPP,
|
||||
CSHARP,
|
||||
CSS,
|
||||
FORTRAN,
|
||||
GDSCRIPT,
|
||||
GO,
|
||||
HTML,
|
||||
JAVA,
|
||||
JAVASCRIPT,
|
||||
JSON,
|
||||
JULIA,
|
||||
KOTLIN,
|
||||
LUA,
|
||||
MARKDOWN,
|
||||
MATHEMATICA,
|
||||
MATLAB,
|
||||
PHP,
|
||||
POWERSHELL,
|
||||
PROLOG,
|
||||
PYTHON,
|
||||
R,
|
||||
RUBY,
|
||||
RUST,
|
||||
SQL,
|
||||
SWIFT,
|
||||
TYPESCRIPT,
|
||||
XML,
|
||||
|
||||
OTHER,
|
||||
}
|
||||
@@ -31,7 +31,7 @@ public partial class Home : ComponentBase
|
||||
private static readonly TextItem[] ITEMS_ADVANTAGES =
|
||||
[
|
||||
new TextItem("Free of charge", "The app is free to use, both for personal and commercial purposes."),
|
||||
new TextItem("Independence", "Users are not tied to any single provider. The initial version supports OpenAI models (like GPT-4o, GPT-4, GPT-4 Turbo, etc.). Future versions will support other providers such as Mistral or Google Gemini."),
|
||||
new TextItem("Independence", "You are not tied to any single provider. Instead, you might choose the provider that best suits your needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), and self-hosted models using llama.cpp, ollama, LM Studio, or Fireworks. Support for Google Gemini and Replicate is planned."),
|
||||
new TextItem("Unrestricted usage", "Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API."),
|
||||
new TextItem("Cost-effective", "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."),
|
||||
new TextItem("Privacy", "The data entered into the app is not used for training by the providers since we are using the provider's API."),
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@page "/assistant/icons"
|
||||
@using AIStudio.Settings
|
||||
@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"/>
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudSelect T="IconSources" @bind-Value="@this.selectedIconSource" AdornmentIcon="@Icons.Material.Filled.Source" Adornment="Adornment.Start" Label="Your icon source" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (var source in Enum.GetValues<IconSources>())
|
||||
{
|
||||
<MudSelectItem Value="@source">@source.Name()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.selectedIconSource is not IconSources.GENERIC)
|
||||
{
|
||||
<MudButton Href="@this.selectedIconSource.URL()" Target="_blank" Variant="Variant.Filled" Size="Size.Medium">Open website</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudSelect T="Provider" @bind-Value="@this.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>
|
||||
|
||||
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.FindIcon()">
|
||||
Find icon
|
||||
</MudButton>
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace AIStudio.Components.Pages.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 string Title => "Icon Finder";
|
||||
|
||||
protected override string Description =>
|
||||
"""
|
||||
Finding the right icon for a context, such as for a piece of text, is not easy. The first challenge:
|
||||
You need to extract a concept from your context, such as from a text. Let's take an example where
|
||||
your text contains statements about multiple departments. The sought-after concept could be "departments."
|
||||
The next challenge is that we need to anticipate the bias of the icon designers: under the search term
|
||||
"departments," there may be no relevant icons or only unsuitable ones. Depending on the icon source,
|
||||
it might be more effective to search for "buildings," for instance. LLMs assist you with both steps.
|
||||
""";
|
||||
|
||||
protected override string SystemPrompt =>
|
||||
"""
|
||||
I can search for icons using US English keywords. Please help me come up with the right search queries.
|
||||
I don't want you to translate my requests word-for-word into US English. Instead, you should provide keywords
|
||||
that are likely to yield suitable icons. For example, I might ask for an icon about departments, but icons
|
||||
related to the keyword "buildings" might be the best match. Provide your keywords in a Markdown list without
|
||||
quotation marks.
|
||||
""";
|
||||
|
||||
private string? ValidatingContext(string context)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(context))
|
||||
return "Please provide a context. This will help the AI to find the right icon. You might type just a keyword or copy a sentence from your text, e.g., from a slide where you want to use the icon.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task FindIcon()
|
||||
{
|
||||
await this.form!.Validate();
|
||||
if (!this.inputIsValid)
|
||||
return;
|
||||
|
||||
this.CreateChatThread();
|
||||
var time = this.AddUserRequest(
|
||||
$"""
|
||||
{this.selectedIconSource.Prompt()} I search for an icon for the following context:
|
||||
|
||||
```
|
||||
{this.inputContext}
|
||||
```
|
||||
""");
|
||||
|
||||
await this.AddAIResponseAsync(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace AIStudio.Components.Pages.IconFinder;
|
||||
|
||||
public static class IconSourceExtensions
|
||||
{
|
||||
public static string Name(this IconSources iconSource) => iconSource switch
|
||||
{
|
||||
IconSources.FLAT_ICON => "Flaticon",
|
||||
IconSources.FONT_AWESOME => "Font Awesome",
|
||||
IconSources.MATERIAL_ICONS => "Material Icons",
|
||||
IconSources.FEATHER_ICONS => "Feather Icons",
|
||||
IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons",
|
||||
IconSources.ICONS8 => "Icons8",
|
||||
|
||||
_ => "Generic",
|
||||
};
|
||||
|
||||
public static string Prompt(this IconSources iconSource) => iconSource switch
|
||||
{
|
||||
IconSources.FLAT_ICON => "My icon source is Flaticon.",
|
||||
IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.",
|
||||
IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.",
|
||||
IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.",
|
||||
IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.",
|
||||
IconSources.ICONS8 => "I look for an icon on Icons8.",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
public static string URL(this IconSources iconSource) => iconSource switch
|
||||
{
|
||||
IconSources.FLAT_ICON => "https://www.flaticon.com/",
|
||||
IconSources.FONT_AWESOME => "https://fontawesome.com/",
|
||||
IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/",
|
||||
IconSources.FEATHER_ICONS => "https://feathericons.com/",
|
||||
IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/",
|
||||
IconSources.ICONS8 => "https://icons8.com/",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Components.Pages.IconFinder;
|
||||
|
||||
public enum IconSources
|
||||
{
|
||||
GENERIC,
|
||||
|
||||
ICONS8,
|
||||
FLAT_ICON,
|
||||
FONT_AWESOME,
|
||||
MATERIAL_ICONS,
|
||||
FEATHER_ICONS,
|
||||
BOOTSTRAP_ICONS,
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
@page "/settings"
|
||||
@using AIStudio.Components.Pages.Coding
|
||||
@using AIStudio.Components.Pages.TextSummarizer
|
||||
@using AIStudio.Provider
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Tools
|
||||
@using Host = AIStudio.Provider.SelfHosted.Host
|
||||
|
||||
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
|
||||
|
||||
@@ -11,7 +17,7 @@
|
||||
<col style="width: 12em;"/>
|
||||
<col style="width: 12em;"/>
|
||||
<col/>
|
||||
<col style="width: 20em;"/>
|
||||
<col style="width: 34em;"/>
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh>#</MudTh>
|
||||
@@ -24,12 +30,28 @@
|
||||
<MudTd>@context.Num</MudTd>
|
||||
<MudTd>@context.InstanceName</MudTd>
|
||||
<MudTd>@context.UsedProvider</MudTd>
|
||||
<MudTd>@context.Model</MudTd>
|
||||
<MudTd>
|
||||
@if (context.UsedProvider is not Providers.SELF_HOSTED)
|
||||
{
|
||||
@this.GetProviderModelName(context)
|
||||
}
|
||||
else if (context.UsedProvider is Providers.SELF_HOSTED && context.Host is not Host.LLAMACPP)
|
||||
{
|
||||
@this.GetProviderModelName(context)
|
||||
}
|
||||
else
|
||||
{
|
||||
@("as selected by provider")
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd Style="text-align: left;">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="mr-2" OnClick="() => this.EditProvider(context)">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@this.GetProviderDashboardURL(context.UsedProvider)" Target="_blank" Disabled="@(!this.HasDashboard(context.UsedProvider))">
|
||||
Open Dashboard
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditProvider(context)">
|
||||
Edit
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="mr-2" OnClick="() => this.DeleteProvider(context)">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteProvider(context)">
|
||||
Delete
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
@@ -41,16 +63,91 @@
|
||||
<MudText Typo="Typo.h6" Class="mt-3">No providers configured yet.</MudText>
|
||||
}
|
||||
|
||||
<MudButton
|
||||
Variant="Variant.Filled" Color="@Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.AddRoad"
|
||||
Class="mt-3 mb-6" OnClick="@this.AddProvider">
|
||||
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddProvider">
|
||||
Add Provider
|
||||
</MudButton>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">Options</MudText>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">App Options</MudText>
|
||||
<ConfigurationOption OptionDescription="Save energy?" LabelOn="Energy saving is enabled" LabelOff="Energy saving is disabled" State="@(() => this.SettingsManager.ConfigurationData.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.IsSavingEnergy = updatedState)" OptionHelp="When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available."/>
|
||||
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." />
|
||||
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
|
||||
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">Chat Options</MudText>
|
||||
<ConfigurationSelect OptionDescription="Shortcut to send input" SelectedValue="@(() => this.SettingsManager.ConfigurationData.ShortcutSendBehavior)" Data="@ConfigurationSelectDataFactory.GetSendBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ShortcutSendBehavior = selectedValue)" OptionHelp="Do you want to use any shortcut to send your input?"/>
|
||||
<ConfigurationOption OptionDescription="Preselect chat options?" LabelOn="Chat options are preselected" LabelOff="No chat options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectChatOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectChatOptions = updatedState)" OptionHelp="When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider."/>
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectChatOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedChatProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedChatProvider = selectedValue)"/>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">Workspace Options</MudText>
|
||||
<ConfigurationSelect OptionDescription="Workspace behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageBehavior = selectedValue)" OptionHelp="Should we store your chats?"/>
|
||||
<ConfigurationSelect OptionDescription="Workspace maintenance" SelectedValue="@(() => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy)" Data="@ConfigurationSelectDataFactory.GetWorkspaceStorageTemporaryMaintenancePolicyData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.WorkspaceStorageTemporaryMaintenancePolicy = selectedValue)" OptionHelp="If and when should we delete your temporary chats?"/>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">Assistant Options</MudText>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">Icon Finder Options</MudText>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="Preselect icon options?" LabelOn="Icon options are preselected" LabelOff="No icon options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectIconOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectIconOptions = updatedState)" OptionHelp="When enabled, you can preselect the icon options. This is might be useful when you prefer a specific icon source or LLM model."/>
|
||||
<ConfigurationSelect OptionDescription="Preselect the icon source" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectIconOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedIconSource)" Data="@ConfigurationSelectDataFactory.GetIconSourcesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedIconSource = selectedValue)" OptionHelp="Which icon source should be preselected?"/>
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectIconOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedIconProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedIconProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">Translator Options</MudText>
|
||||
<ConfigurationSlider T="int" OptionDescription="How fast should the live translation react?" Min="500" Max="3_000" Step="100" Unit="milliseconds" Value="@(() => this.SettingsManager.ConfigurationData.LiveTranslationDebounceIntervalMilliseconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.LiveTranslationDebounceIntervalMilliseconds = updatedValue)"/>
|
||||
<ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.HideWebContentReaderForTranslation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.HideWebContentReaderForTranslation = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="Preselect translator options?" LabelOn="Translator options are preselected" LabelOff="No translator options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectTranslationOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectTranslationOptions = updatedState)" OptionHelp="When enabled, you can preselect the translator options. This is might be useful when you prefer a specific target language or LLM model."/>
|
||||
<ConfigurationOption OptionDescription="Preselect the web content reader?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions || this.SettingsManager.ConfigurationData.HideWebContentReaderForTranslation)" LabelOn="Web content reader is preselected" LabelOff="Web content reader is not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTranslation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTranslation = updatedState)" OptionHelp="When enabled, the web content reader is preselected. This is might be useful when you prefer to load content from the web very often."/>
|
||||
<ConfigurationOption OptionDescription="Preselect the content cleaner agent?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions || this.SettingsManager.ConfigurationData.HideWebContentReaderForTranslation)" LabelOn="Content cleaner agent is preselected" LabelOff="Content cleaner agent is not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTranslation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTranslation = updatedState)" OptionHelp="When enabled, the content cleaner agent is preselected. This is might be useful when you prefer to clean up the content before translating it."/>
|
||||
<ConfigurationOption OptionDescription="Preselect live translation?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions)" LabelOn="Live translation is preselected" LabelOff="Live translation is not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectLiveTranslation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectLiveTranslation = updatedState)" />
|
||||
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedTranslationTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedTranslationTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
|
||||
@if (this.SettingsManager.ConfigurationData.PreselectedTranslationTargetLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.PreselectTranslationOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.PreselectTranslationOtherLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTranslationOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedTranslationProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedTranslationProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">Coding Options</MudText>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="Preselect coding options?" LabelOn="Coding options are preselected" LabelOff="No coding options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectCodingOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectCodingOptions = updatedState)" OptionHelp="When enabled, you can preselect the coding options. This is might be useful when you prefer a specific programming language or LLM model."/>
|
||||
<ConfigurationOption OptionDescription="Preselect compiler messages?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectCodingOptions)" LabelOn="Compiler messages are preselected" LabelOff="Compiler messages are not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectCodingCompilerMessages)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectCodingCompilerMessages = updatedState)" />
|
||||
<ConfigurationSelect OptionDescription="Preselect a programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectCodingOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedCodingLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonCodingLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedCodingLanguage = selectedValue)" OptionHelp="Which programming language should be preselected for added contexts?"/>
|
||||
@if (this.SettingsManager.ConfigurationData.PreselectedCodingLanguage is CommonCodingLanguages.OTHER)
|
||||
{
|
||||
<ConfigurationText OptionDescription="Preselect another programming language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectCodingOptions)" Icon="@Icons.Material.Filled.Code" Text="@(() => this.SettingsManager.ConfigurationData.PreselectedCodingOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.PreselectedCodingOtherLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectCodingOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedCodingProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedCodingProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">Text Summarizer Options</MudText>
|
||||
<ConfigurationOption OptionDescription="Hide the web content reader?" LabelOn="Web content reader is hidden" LabelOff="Web content reader is shown" State="@(() => this.SettingsManager.ConfigurationData.HideWebContentReaderForTextSummarizer)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.HideWebContentReaderForTextSummarizer = updatedState)" OptionHelp="When activated, the web content reader is hidden and cannot be used. As a result, the user interface becomes a bit easier to use."/>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="Preselect summarizer options?" LabelOn="Summarizer options are preselected" LabelOff="No summarizer options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions = updatedState)" OptionHelp="When enabled, you can preselect the text summarizer options. This is might be useful when you prefer a specific language, complexity, or LLM."/>
|
||||
<ConfigurationOption OptionDescription="Preselect the web content reader?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions || this.SettingsManager.ConfigurationData.HideWebContentReaderForTextSummarizer)" LabelOn="Web content reader is preselected" LabelOff="Web content reader is not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTextSummarizer)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTextSummarizer = updatedState)" OptionHelp="When enabled, the web content reader is preselected. This is might be useful when you prefer to load content from the web very often."/>
|
||||
<ConfigurationOption OptionDescription="Preselect the content cleaner agent?" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions || this.SettingsManager.ConfigurationData.HideWebContentReaderForTextSummarizer)" LabelOn="Content cleaner agent is preselected" LabelOff="Content cleaner agent is not preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTextSummarizer)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTextSummarizer = updatedState)" OptionHelp="When enabled, the content cleaner agent is preselected. This is might be useful when you prefer to clean up the content before summarize it."/>
|
||||
<ConfigurationSelect OptionDescription="Preselect the target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerTargetLanguage = selectedValue)" OptionHelp="Which target language should be preselected?"/>
|
||||
@if (this.SettingsManager.ConfigurationData.PreselectedTextSummarizerTargetLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<ConfigurationText OptionDescription="Preselect another target language" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerOtherLanguage = updatedText)"/>
|
||||
}
|
||||
<ConfigurationSelect OptionDescription="Preselect the summarizer complexity" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerComplexity)" Data="@ConfigurationSelectDataFactory.GetComplexityData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerComplexity = selectedValue)" OptionHelp="Which summarizer complexity should be preselected?"/>
|
||||
@if(this.SettingsManager.ConfigurationData.PreselectedTextSummarizerComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
|
||||
{
|
||||
<ConfigurationText OptionDescription="Preselect your expertise" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" Icon="@Icons.Material.Filled.Person" Text="@(() => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerExpertInField)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerExpertInField = updatedText)"/>
|
||||
}
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedTextSummarizerProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-3">LLM Agent Options</MudText>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3">Text Content Cleaner Agent</MudText>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
Use Case: this agent is used to clean up text content. It extracts the main content, removes advertisements and other irrelevant things,
|
||||
and attempts to convert relative links into absolute links so that they can be used.
|
||||
</MudText>
|
||||
<ConfigurationOption OptionDescription="Preselect text content cleaner options?" LabelOn="Options are preselected" LabelOff="No options are preselected" State="@(() => this.SettingsManager.ConfigurationData.PreselectAgentTextContentCleanerOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.PreselectAgentTextContentCleanerOptions = updatedState)" OptionHelp="When enabled, you can preselect some agent options. This is might be useful when you prefer a LLM."/>
|
||||
<ConfigurationProviderSelection Data="@this.availableProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.PreselectAgentTextContentCleanerOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.PreselectedAgentTextContentCleanerProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.PreselectedAgentTextContentCleanerProvider = selectedValue)"/>
|
||||
</MudPaper>
|
||||
</MudPaper>
|
||||
</InnerScrolling>
|
||||
@@ -1,13 +1,17 @@
|
||||
using AIStudio.Components.CommonDialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
|
||||
|
||||
// ReSharper disable ClassNeverInstantiated.Global
|
||||
|
||||
namespace AIStudio.Components.Pages;
|
||||
|
||||
public partial class Settings : ComponentBase
|
||||
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
public SettingsManager SettingsManager { get; init; } = null!;
|
||||
@@ -17,18 +21,21 @@ public partial class Settings : ComponentBase
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
private static readonly DialogOptions DIALOG_OPTIONS = new()
|
||||
{
|
||||
CloseOnEscapeKey = true,
|
||||
FullWidth = true, MaxWidth = MaxWidth.Medium,
|
||||
};
|
||||
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
private readonly List<ConfigurationSelectData<string>> availableProviders = new();
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await this.SettingsManager.LoadSettings();
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.CONFIGURATION_CHANGED ]);
|
||||
|
||||
this.UpdateProviders();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@@ -43,16 +50,19 @@ public partial class Settings : ComponentBase
|
||||
{ x => x.IsEditing, false },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Add Provider", dialogParameters, DIALOG_OPTIONS);
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Add Provider", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult.Canceled)
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var addedProvider = (AIStudio.Settings.Provider)dialogResult.Data;
|
||||
var addedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
|
||||
addedProvider = addedProvider with { Num = this.SettingsManager.ConfigurationData.NextProviderNum++ };
|
||||
|
||||
this.SettingsManager.ConfigurationData.Providers.Add(addedProvider);
|
||||
this.UpdateProviders();
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task EditProvider(AIStudio.Settings.Provider provider)
|
||||
@@ -64,15 +74,18 @@ public partial class Settings : ComponentBase
|
||||
{ x => x.DataInstanceName, provider.InstanceName },
|
||||
{ x => x.DataProvider, provider.UsedProvider },
|
||||
{ x => x.DataModel, provider.Model },
|
||||
{ x => x.DataHostname, provider.Hostname },
|
||||
{ x => x.IsSelfHosted, provider.IsSelfHosted },
|
||||
{ x => x.IsEditing, true },
|
||||
{ x => x.DataHost, provider.Host },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit Provider", dialogParameters, DIALOG_OPTIONS);
|
||||
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>("Edit Provider", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult.Canceled)
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data;
|
||||
var editedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
|
||||
|
||||
// Set the provider number if it's not set. This is important for providers
|
||||
// added before we started saving the provider number.
|
||||
@@ -80,7 +93,10 @@ public partial class Settings : ComponentBase
|
||||
editedProvider = editedProvider with { Num = this.SettingsManager.ConfigurationData.NextProviderNum++ };
|
||||
|
||||
this.SettingsManager.ConfigurationData.Providers[this.SettingsManager.ConfigurationData.Providers.IndexOf(provider)] = editedProvider;
|
||||
this.UpdateProviders();
|
||||
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task DeleteProvider(AIStudio.Settings.Provider provider)
|
||||
@@ -90,18 +106,85 @@ public partial class Settings : ComponentBase
|
||||
{ "Message", $"Are you sure you want to delete the provider '{provider.InstanceName}'?" },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Provider", dialogParameters, DIALOG_OPTIONS);
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Provider", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult.Canceled)
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName);
|
||||
var providerInstance = provider.CreateProvider();
|
||||
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
||||
if(deleteSecretResponse.Success)
|
||||
{
|
||||
this.SettingsManager.ConfigurationData.Providers.Remove(provider);
|
||||
await this.SettingsManager.StoreSettings();
|
||||
}
|
||||
|
||||
this.UpdateProviders();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private bool HasDashboard(Providers provider) => provider switch
|
||||
{
|
||||
Providers.OPEN_AI => true,
|
||||
Providers.MISTRAL => true,
|
||||
Providers.ANTHROPIC => true,
|
||||
Providers.FIREWORKS => true,
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private string GetProviderDashboardURL(Providers provider) => provider switch
|
||||
{
|
||||
Providers.OPEN_AI => "https://platform.openai.com/usage",
|
||||
Providers.MISTRAL => "https://console.mistral.ai/usage/",
|
||||
Providers.ANTHROPIC => "https://console.anthropic.com/settings/plans",
|
||||
Providers.FIREWORKS => "https://fireworks.ai/account/billing",
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
private string GetProviderModelName(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
const int MAX_LENGTH = 36;
|
||||
var modelName = provider.Model.ToString();
|
||||
return modelName.Length > MAX_LENGTH ? "[...] " + modelName[^Math.Min(MAX_LENGTH, modelName.Length)..] : modelName;
|
||||
}
|
||||
|
||||
private void UpdateProviders()
|
||||
{
|
||||
this.availableProviders.Clear();
|
||||
foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
this.availableProviders.Add(new (provider.InstanceName, provider.Id));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.CONFIGURATION_CHANGED:
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.MessageBus.Unregister(this);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
@page "/assistant/summarizer"
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Tools
|
||||
@inherits AssistantBaseCore
|
||||
|
||||
@if (!this.SettingsManager.ConfigurationData.HideWebContentReaderForTextSummarizer)
|
||||
{
|
||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions && this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTextSummarizer)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.PreselectTextSummarizerOptions && this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTextSummarizer)"/>
|
||||
}
|
||||
|
||||
<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"/>
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudSelect T="CommonLanguages" @bind-Value="@this.selectedTargetLanguage" AdornmentIcon="@Icons.Material.Filled.Translate" Adornment="Adornment.Start" Label="Target language" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (var targetLanguage in Enum.GetValues<CommonLanguages>())
|
||||
{
|
||||
<MudSelectItem Value="@targetLanguage">@targetLanguage.Name()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.selectedTargetLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<MudTextField T="string" @bind-Text="@this.customTargetLanguage" Validation="@this.ValidateCustomLanguage" Label="Custom target language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudSelect T="Complexity" @bind-Value="@this.selectedComplexity" AdornmentIcon="@Icons.Material.Filled.Layers" Adornment="Adornment.Start" Label="Target complexity" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (var targetComplexity in Enum.GetValues<Complexity>())
|
||||
{
|
||||
<MudSelectItem Value="@targetComplexity">@targetComplexity.Name()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.selectedComplexity is Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS)
|
||||
{
|
||||
<MudTextField T="string" @bind-Text="@this.expertInField" Validation="@this.ValidateExpertInField" Label="Your expertise" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudSelect T="Provider" Disabled="@this.isAgentRunning" @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>
|
||||
|
||||
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SummarizeText()" Disabled="@this.isAgentRunning">
|
||||
Summarize
|
||||
</MudButton>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Components.Pages.TextSummarizer;
|
||||
|
||||
public partial class AssistantTextSummarizer : AssistantBaseCore
|
||||
{
|
||||
protected override string Title => "Text Summarizer";
|
||||
|
||||
protected override string Description =>
|
||||
"""
|
||||
Summarize long text into a shorter version while retaining the main points.
|
||||
You might want to change the language of the summary to make it more readable.
|
||||
It is also possible to change the complexity of the summary to make it
|
||||
easy to understand.
|
||||
""";
|
||||
|
||||
protected override string SystemPrompt =>
|
||||
"""
|
||||
You get a long text as input. The user wants to get a summary of the text.
|
||||
The user might want to change the language of the summary. In this case,
|
||||
you should provide a summary in the requested language. Eventually, the user
|
||||
want to change the complexity of the text. In this case, you should provide
|
||||
a summary with the requested complexity. In any case, do not add any information.
|
||||
""";
|
||||
|
||||
private string inputText = string.Empty;
|
||||
private bool isAgentRunning;
|
||||
private CommonLanguages selectedTargetLanguage;
|
||||
private string customTargetLanguage = string.Empty;
|
||||
private Complexity selectedComplexity;
|
||||
private string expertInField = string.Empty;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string? ValidatingText(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? ValidateExpertInField(string field)
|
||||
{
|
||||
if(this.selectedComplexity == Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS && string.IsNullOrWhiteSpace(field))
|
||||
return "Please provide your field of expertise.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task SummarizeText()
|
||||
{
|
||||
await this.form!.Validate();
|
||||
if (!this.inputIsValid)
|
||||
return;
|
||||
|
||||
this.CreateChatThread();
|
||||
var time = this.AddUserRequest(
|
||||
$"""
|
||||
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
|
||||
{this.selectedComplexity.Prompt(this.expertInField)}
|
||||
|
||||
Please summarize the following text:
|
||||
|
||||
```
|
||||
{this.inputText}
|
||||
```
|
||||
""");
|
||||
|
||||
await this.AddAIResponseAsync(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Components.Pages.TextSummarizer;
|
||||
|
||||
public static class CommonLanguagePrompts
|
||||
{
|
||||
public static string Prompt(this CommonLanguages language, string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.AS_IS => "Do not change the language of the text.",
|
||||
CommonLanguages.OTHER => $"Output you summary in {customLanguage}.",
|
||||
|
||||
_ => $"Output your summary in {language.Name()} ({language}).",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Components.Pages.TextSummarizer;
|
||||
|
||||
public enum Complexity
|
||||
{
|
||||
NO_CHANGE,
|
||||
|
||||
SIMPLE_LANGUAGE,
|
||||
TEEN_LANGUAGE,
|
||||
EVERYDAY_LANGUAGE,
|
||||
POPULAR_SCIENCE_LANGUAGE,
|
||||
SCIENTIFIC_LANGUAGE_FIELD_EXPERTS,
|
||||
SCIENTIFIC_LANGUAGE_OTHER_EXPERTS,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace AIStudio.Components.Pages.TextSummarizer;
|
||||
|
||||
public static class ComplexityExtensions
|
||||
{
|
||||
public static string Name(this Complexity complexity) => complexity switch
|
||||
{
|
||||
Complexity.NO_CHANGE => "No change in complexity",
|
||||
|
||||
Complexity.SIMPLE_LANGUAGE => "Simple language, e.g., for children",
|
||||
Complexity.TEEN_LANGUAGE => "Teen language, e.g., for teenagers",
|
||||
Complexity.EVERYDAY_LANGUAGE => "Everyday language, e.g., for adults",
|
||||
Complexity.POPULAR_SCIENCE_LANGUAGE => "Popular science language, e.g., for people interested in science",
|
||||
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Scientific language for experts in this field",
|
||||
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => "Scientific language for experts from other fields (interdisciplinary)",
|
||||
|
||||
_ => "No change in complexity",
|
||||
};
|
||||
|
||||
public static string Prompt(this Complexity complexity, string expertInField) => complexity switch
|
||||
{
|
||||
Complexity.NO_CHANGE => "Do not change the complexity of the text.",
|
||||
|
||||
Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.",
|
||||
Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.",
|
||||
Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.",
|
||||
Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.",
|
||||
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.",
|
||||
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.",
|
||||
|
||||
_ => "Do not change the complexity of the text.",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
@page "/assistant/translation"
|
||||
@using AIStudio.Settings
|
||||
@using AIStudio.Tools
|
||||
@inherits AssistantBaseCore
|
||||
|
||||
@if (!this.SettingsManager.ConfigurationData.HideWebContentReaderForTranslation)
|
||||
{
|
||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.providerSettings" @bind-AgentIsRunning="@this.isAgentRunning" Preselect="@(this.SettingsManager.ConfigurationData.PreselectTranslationOptions && this.SettingsManager.ConfigurationData.PreselectWebContentReaderForTranslation)" PreselectContentCleanerAgent="@(this.SettingsManager.ConfigurationData.PreselectTranslationOptions && this.SettingsManager.ConfigurationData.PreselectContentCleanerAgentForTranslation)"/>
|
||||
}
|
||||
|
||||
<MudField Label="Live translation" Variant="Variant.Outlined" Class="mb-3">
|
||||
<MudSwitch T="bool" @bind-Value="@this.liveTranslation" Color="Color.Primary">
|
||||
@(this.liveTranslation ? "Live translation" : "No live translation")
|
||||
</MudSwitch>
|
||||
</MudField>
|
||||
|
||||
@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="1_000" 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"/>
|
||||
}
|
||||
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
|
||||
<MudSelect T="CommonLanguages" @bind-Value="@this.selectedTargetLanguage" Validation="@this.ValidatingTargetLanguage" AdornmentIcon="@Icons.Material.Filled.Translate" Adornment="Adornment.Start" Label="Target language" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
@foreach (var targetLanguage in Enum.GetValues<CommonLanguages>())
|
||||
{
|
||||
if (targetLanguage is CommonLanguages.AS_IS)
|
||||
{
|
||||
<MudSelectItem Value="@targetLanguage">Please select the target language</MudSelectItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSelectItem Value="@targetLanguage">@targetLanguage.Name()</MudSelectItem>
|
||||
}
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.selectedTargetLanguage is CommonLanguages.OTHER)
|
||||
{
|
||||
<MudTextField T="string" @bind-Text="@this.customTargetLanguage" Validation="@this.ValidateCustomLanguage" Label="Custom target language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
<MudSelect T="Provider" @bind-Value="@this.providerSettings" Disabled="@this.isAgentRunning" Validation="@this.ValidatingProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined">
|
||||
@foreach (var provider in this.SettingsManager.ConfigurationData.Providers)
|
||||
{
|
||||
<MudSelectItem Value="@provider"/>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudButton Disabled="@this.isAgentRunning" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.TranslateText(force: true)">
|
||||
Translate
|
||||
</MudButton>
|
||||
@@ -0,0 +1,92 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Components.Pages.Translation;
|
||||
|
||||
public partial class AssistantTranslation : AssistantBaseCore
|
||||
{
|
||||
protected override string Title => "Translation";
|
||||
|
||||
protected override string Description =>
|
||||
"""
|
||||
Translate text from one language to another.
|
||||
""";
|
||||
|
||||
protected override string SystemPrompt =>
|
||||
"""
|
||||
You get text in a source language as input. The user wants to get the text translated into a target language.
|
||||
Provide the translation in the requested language. Do not add any information. Correct any spelling or grammar mistakes.
|
||||
Do not ask for additional information. Do not mirror the user's language. Do not mirror the task. When the target
|
||||
language requires, e.g., shorter sentences, you should split the text into shorter sentences.
|
||||
""";
|
||||
|
||||
private bool liveTranslation;
|
||||
private bool isAgentRunning;
|
||||
private string inputText = string.Empty;
|
||||
private string inputTextLastTranslation = string.Empty;
|
||||
private CommonLanguages selectedTargetLanguage;
|
||||
private string customTargetLanguage = string.Empty;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string? ValidatingText(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? ValidatingTargetLanguage(CommonLanguages language)
|
||||
{
|
||||
if(language == CommonLanguages.AS_IS)
|
||||
return "Please select a target language.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? ValidateCustomLanguage(string language)
|
||||
{
|
||||
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
|
||||
return "Please provide a custom language.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task TranslateText(bool force)
|
||||
{
|
||||
if (!this.inputIsValid)
|
||||
return;
|
||||
|
||||
if(!force && this.inputText == this.inputTextLastTranslation)
|
||||
return;
|
||||
|
||||
this.inputTextLastTranslation = this.inputText;
|
||||
this.CreateChatThread();
|
||||
var time = this.AddUserRequest(
|
||||
$"""
|
||||
{this.selectedTargetLanguage.Prompt(this.customTargetLanguage)}
|
||||
|
||||
The given text is:
|
||||
|
||||
---
|
||||
{this.inputText}
|
||||
""");
|
||||
|
||||
await this.AddAIResponseAsync(time);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Components.Pages.Translation;
|
||||
|
||||
public static class CommonLanguageExtension
|
||||
{
|
||||
public static string Prompt(this CommonLanguages language, string customLanguage) => language switch
|
||||
{
|
||||
CommonLanguages.OTHER => $"Translate the text in {customLanguage}.",
|
||||
|
||||
_ => $"Translate the given text in {language.Name()} ({language}).",
|
||||
};
|
||||
}
|
||||
@@ -7,4 +7,5 @@
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudDialogProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudSnackbarProvider />
|
||||
@@ -45,9 +45,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.6" />
|
||||
<PackageReference Include="MudBlazor" Version="6.20.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.7" />
|
||||
<PackageReference Include="MudBlazor" Version="7.4.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="1.0.2" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="4.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Read the meta data file -->
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using AIStudio;
|
||||
using AIStudio.Agents;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
@@ -25,10 +26,15 @@ builder.Services.AddMudServices(config =>
|
||||
});
|
||||
|
||||
builder.Services.AddMudMarkdownServices();
|
||||
builder.Services.AddSingleton(MessageBus.INSTANCE);
|
||||
builder.Services.AddSingleton<Rust>();
|
||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||
builder.Services.AddSingleton<SettingsManager>();
|
||||
builder.Services.AddSingleton<Random>();
|
||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||
builder.Services.AddTransient<HTMLParser>();
|
||||
builder.Services.AddTransient<AgentTextContentCleaner>();
|
||||
builder.Services.AddHostedService<UpdateService>();
|
||||
builder.Services.AddHostedService<TemporaryChatService>();
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddHubOptions(options =>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using AIStudio.Provider.OpenAI;
|
||||
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
/// <summary>
|
||||
/// The Anthropic chat request model.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use for chat completion.</param>
|
||||
/// <param name="Messages">The chat messages.</param>
|
||||
/// <param name="MaxTokens">The maximum number of tokens to generate.</param>
|
||||
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||
/// <param name="System">The system prompt for the chat completion.</param>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<Message> Messages,
|
||||
int MaxTokens,
|
||||
bool Stream,
|
||||
string System
|
||||
);
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
public string Id => "Anthropic";
|
||||
|
||||
public string InstanceName { get; set; } = "Anthropic";
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
// Get the API key:
|
||||
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
|
||||
if(!requestedSecret.Success)
|
||||
yield break;
|
||||
|
||||
// Prepare the Anthropic HTTP chat request:
|
||||
var chatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||
{
|
||||
Model = chatModel.Id,
|
||||
|
||||
// Build the messages:
|
||||
Messages = [..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
|
||||
{
|
||||
Role = n.Role switch
|
||||
{
|
||||
ChatRole.USER => "user",
|
||||
ChatRole.AI => "assistant",
|
||||
ChatRole.AGENT => "assistant",
|
||||
|
||||
_ => "user",
|
||||
},
|
||||
|
||||
Content = n.Content switch
|
||||
{
|
||||
ContentText text => text.Text,
|
||||
_ => string.Empty,
|
||||
}
|
||||
}).ToList()],
|
||||
|
||||
System = chatThread.SystemPrompt,
|
||||
MaxTokens = 4_096,
|
||||
|
||||
// Right now, we only support streaming completions:
|
||||
Stream = true,
|
||||
}, JSON_SERIALIZER_OPTIONS);
|
||||
|
||||
// Build the HTTP post request:
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
|
||||
|
||||
// Set the authorization header:
|
||||
request.Headers.Add("x-api-key", requestedSecret.Secret);
|
||||
|
||||
// Set the Anthropic version:
|
||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||
|
||||
// Set the content:
|
||||
request.Content = new StringContent(chatRequest, Encoding.UTF8, "application/json");
|
||||
|
||||
// Send the request with the ResponseHeadersRead option.
|
||||
// This allows us to read the stream as soon as the headers are received.
|
||||
// This is important because we want to stream the responses.
|
||||
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
|
||||
// Open the response stream:
|
||||
var stream = await response.Content.ReadAsStreamAsync(token);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
var streamReader = new StreamReader(stream);
|
||||
|
||||
// Read the stream, line by line:
|
||||
while(!streamReader.EndOfStream)
|
||||
{
|
||||
// Check if the token is canceled:
|
||||
if(token.IsCancellationRequested)
|
||||
yield break;
|
||||
|
||||
// Read the next line:
|
||||
var line = await streamReader.ReadLineAsync(token);
|
||||
|
||||
// Skip empty lines:
|
||||
if(string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
// Check for the end of the stream:
|
||||
if(line.StartsWith("event: message_stop", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
// Ignore any type except "content_block_delta":
|
||||
if(!line.Contains("\"content_block_delta\"", StringComparison.InvariantCulture))
|
||||
continue;
|
||||
|
||||
ResponseStreamLine anthropicResponse;
|
||||
try
|
||||
{
|
||||
// We know that the line starts with "data: ". Hence, we can
|
||||
// skip the first 6 characters to get the JSON data after that.
|
||||
var jsonData = line[6..];
|
||||
|
||||
// Deserialize the JSON data:
|
||||
anthropicResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip invalid JSON data:
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty responses:
|
||||
if(anthropicResponse == default || string.IsNullOrWhiteSpace(anthropicResponse.Delta.Text))
|
||||
continue;
|
||||
|
||||
// Yield the response:
|
||||
yield return anthropicResponse.Delta.Text;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(new[]
|
||||
{
|
||||
new Model("claude-3-5-sonnet-20240620"),
|
||||
new Model("claude-3-opus-20240229"),
|
||||
new Model("claude-3-sonnet-20240229"),
|
||||
new Model("claude-3-haiku-20240307"),
|
||||
}.AsEnumerable());
|
||||
}
|
||||
|
||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
/// <inheritdoc />
|
||||
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(Enumerable.Empty<Model>());
|
||||
}
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// ReSharper disable NotAccessedPositionalProperty.Global
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a response stream line.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the response line.</param>
|
||||
/// <param name="Index">The index of the response line.</param>
|
||||
/// <param name="Delta">The delta of the response line.</param>
|
||||
public readonly record struct ResponseStreamLine(string Type, int Index, Delta Delta);
|
||||
|
||||
/// <summary>
|
||||
/// The delta object of a response line.
|
||||
/// </summary>
|
||||
/// <param name="Type">The type of the delta.</param>
|
||||
/// <param name="Text">The text of the delta.</param>
|
||||
public readonly record struct Delta(string Type, string Text);
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
/// <summary>
|
||||
/// The Fireworks chat request model.
|
||||
/// </summary>
|
||||
/// <param name="Model">Which model to use for chat completion.</param>
|
||||
/// <param name="Messages">The chat messages.</param>
|
||||
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||
public readonly record struct ChatRequest(
|
||||
string Model,
|
||||
IList<Message> Messages,
|
||||
bool Stream
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
/// <summary>
|
||||
/// Chat message model.
|
||||
/// </summary>
|
||||
/// <param name="Content">The text content of the message.</param>
|
||||
/// <param name="Role">The role of the message.</param>
|
||||
public readonly record struct Message(string Content, string Role);
|
||||
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user