Compare commits

...
Author SHA1 Message Date
nilskruthoffandThorsten Sommer 052868b24e Added a settings dialog with organization-managed defaults to the Assistant Builder (#908)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-10-22 15:26:04 +02:00
Thorsten Sommer 4695fb3fef Added the prototype badge to the mailbox dialog (#1103) 2026-10-22 14:48:25 +02:00
Peer HogeterpandThorsten Sommer 24c4c31035 Added profile, provider, and workspace preselection to chat templates (#1099)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-10-22 14:15:10 +02:00
Thorsten Sommer ab9df99cf0 Added the page jump for PdfCraft on Windows, macOS, and Linux (#1102) 2026-10-22 12:08:42 +02:00
Thorsten Sommer ba99bf6fae Scaled down and converted images before sending them to AI models (#1101) 2026-10-22 10:57:04 +02:00
Thorsten Sommer 1c8632bbe0 Fixed the Batch Processing assistant with an output folder above the input folder (#1098) 2026-10-21 15:51:22 +02:00
Thorsten Sommer 7fbb894d5b Upgraded to Rust v1.99.1 (#1097) 2026-10-21 14:53:16 +02:00
Peer HogeterpandThorsten Sommer e019c24ea2 Allowed the Batch Processing assistant to process images (#1087)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-10-21 13:52:26 +02:00
Paul KoudelkaandThorsten Sommer 5d6e48ef93 Allowed moving local data sources without embedding them again (#1091)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
2026-10-20 18:50:09 +02:00
126 changed files with 6987 additions and 707 deletions

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ startup.env
libpdfium.dylib libpdfium.dylib
libpdfium.so libpdfium.so
libpdfium.dll libpdfium.dll
pdfium.dll
# Ignore qdrant database: # Ignore qdrant database:
qdrant-aarch64-apple-darwin qdrant-aarch64-apple-darwin
@@ -25,6 +25,10 @@
@T("Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '<media-file>.transcript.md' and reused when an interrupted run is continued.") @T("Supported audio and video files are transcribed automatically without an additional dialog. Each transcript is stored next to its media file as '<media-file>.transcript.md' and reused when an interrupted run is continued.")
</MudJustifiedText> </MudJustifiedText>
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start.")
</MudJustifiedText>
<MudStack Class="mb-3"> <MudStack Class="mb-3">
<MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune"> <MudButton OnClick="@this.ToggleExpertSettings" Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Tune">
@(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings")) @(this.showExpertSettings ? T("Hide Expert Settings") : T("Show Expert Settings"))
@@ -1,18 +1,90 @@
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools.Media; using AIStudio.Tools.Media;
using AIStudio.Tools.Rust;
namespace AIStudio.Assistants.BatchProcessing; namespace AIStudio.Assistants.BatchProcessing;
public partial class AssistantBatchProcessing public partial class AssistantBatchProcessing
{ {
/// <summary> /// <summary>
/// Loads a document through the Rust content stream or resolves a persistent /// What one file of the batch gives to the model: either text or an image.
/// transcript for an audio or video file.
/// </summary> /// </summary>
private Task<string?> LoadInputContentAsync(BatchProcessingFileResult fileResult, CancellationToken token) private readonly record struct BatchInput(string Text, FileAttachmentImage? Image);
/// <summary>
/// Loads a document through the Rust content stream, resolves a persistent
/// transcript for an audio or video file, or prepares an image for the model.
/// </summary>
/// <returns>The input for the model, or <c>null</c> when the file already got its final status.</returns>
private async Task<BatchInput?> LoadInputContentAsync(BatchProcessingFileResult fileResult, CancellationToken token)
{ {
return IsTranscribableMedia(fileResult.FilePath) if (IsImage(fileResult.FilePath))
? this.LoadMediaTranscriptAsync(fileResult, token) return await this.LoadImageAsync(fileResult, token) is { } image ? new BatchInput(string.Empty, image) : null;
: this.LoadDocumentContentAsync(fileResult, token);
var text = IsTranscribableMedia(fileResult.FilePath)
? await this.LoadMediaTranscriptAsync(fileResult, token)
: await this.LoadDocumentContentAsync(fileResult, token);
return text is null ? null : new BatchInput(text, null);
}
/// <summary>
/// Prepares an image for the model before the request is built.
/// </summary>
/// <remarks>
/// The request itself would only skip an image it cannot send, with a message, and ask the model
/// anyway. The model would then answer about nothing, and the file would count as done, so that
/// continuing the run would skip it as well. Preparing the image here lets it fail as a file
/// instead, with the reason, e.g., an image format AI Studio cannot decode. The request then finds
/// the prepared image in the cache of ModelImageService, so it is not prepared twice. A failed
/// image is processed again when the run is continued, e.g., with a model that can read images.
///
/// How large an image may be needs no check of its own either: the preparation scales it down
/// until it fits the limit of the provider, and fails only when even that is not enough.
///
/// How many images a model accepts needs no check of its own: every file gets a thread of its own,
/// see CallAIAsync, so each request carries exactly one image, in every round with the model. A
/// limit of one image is therefore always met, and SupportsImageInput already refuses a limit of zero.
///
/// Unlike the text of documents and transcripts, an image does not pass the prompt injection
/// filter of the runtime, which reads text only. Images in a chat do not either. Instructions
/// within an image therefore meet only the safeguards behind the filter: the system prompt tells
/// the model to ignore them, a thread which read internal content, such as a mailbox, may no
/// longer send data out, and without free address choice, its default, the Read Web Page tool
/// refuses an address which appears only in the image.
/// </remarks>
private async Task<FileAttachmentImage?> LoadImageAsync(BatchProcessingFileResult fileResult, CancellationToken token)
{
if (!this.ProviderSettings.SupportsImageInput())
{
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The selected model cannot read images. Select a model with image support to process this image."));
return null;
}
PreparedModelImage prepared;
FileAttachmentImage image;
try
{
image = new FileAttachmentImage(fileResult.FileName, fileResult.FilePath, new FileInfo(fileResult.FilePath).Length);
prepared = await this.ModelImageService.PrepareAsync(image, this.ProviderSettings, token);
}
catch (OperationCanceledException)
{
this.FinishFileResult(fileResult, BatchProcessingFileStatus.CANCELED, T("The batch run was canceled."));
return null;
}
catch (Exception e)
{
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, string.Format(T("Was not able to read the file: {0}"), e.Message), e);
return null;
}
if (prepared.IsPrepared)
return image;
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, prepared.ErrorCode.ToUserMessage(fileResult.FileName));
return null;
} }
private async Task<string?> LoadDocumentContentAsync(BatchProcessingFileResult fileResult, CancellationToken token) private async Task<string?> LoadDocumentContentAsync(BatchProcessingFileResult fileResult, CancellationToken token)
@@ -109,5 +181,7 @@ public partial class AssistantBatchProcessing
private static bool IsTranscribableMedia(string filePath) => PersistentMediaTranscript.IsMediaPath(filePath); private static bool IsTranscribableMedia(string filePath) => PersistentMediaTranscript.IsMediaPath(filePath);
private static bool IsImage(string filePath) => FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE);
private static bool HasReusableTranscript(string mediaFilePath) => MediaSidecarTranscripts.HasReusableTranscript(mediaFilePath); private static bool HasReusableTranscript(string mediaFilePath) => MediaSidecarTranscripts.HasReusableTranscript(mediaFilePath);
} }
@@ -56,6 +56,8 @@ public partial class AssistantBatchProcessing
# Task description # Task description
You are a batch document processing agent. Each request contains exactly one DOCUMENT. You are a batch document processing agent. Each request contains exactly one DOCUMENT.
Your task is to process this DOCUMENT strictly according to the instructions below. Your task is to process this DOCUMENT strictly according to the instructions below.
A DOCUMENT may also be an image attached to the request. Then, the content of the
DOCUMENT is what the image shows, including any text within it.
# Scope and precedence # Scope and precedence
Use only information explicitly contained in the DOCUMENT and the instructions. Use only information explicitly contained in the DOCUMENT and the instructions.
@@ -88,11 +90,20 @@ public partial class AssistantBatchProcessing
"""; """;
} }
private static string BuildImageUserPrompt(string fileName)
{
return $"""
# DOCUMENT
File name: {fileName}
Content: The DOCUMENT is the image attached to this request.
""";
}
/// <param name="fileName">The name of the document being processed.</param> /// <param name="fileName">The name of the document being processed.</param>
/// <param name="fileContent">The content handed to the model.</param> /// <param name="input">The text or the image handed to the model.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The answer of the model, and which tools it used to get there.</returns> /// <returns>The answer of the model, and which tools it used to get there.</returns>
private async Task<(string Answer, string UsedTools)> CallAIAsync(string fileName, string fileContent, CancellationToken token) private async Task<(string Answer, string UsedTools)> CallAIAsync(string fileName, BatchInput input, CancellationToken token)
{ {
// //
// Every file of the batch gets the tools the user picked for the job. The batch builds its // Every file of the batch gets the tools the user picked for the job. The batch builds its
@@ -120,10 +131,9 @@ public partial class AssistantBatchProcessing
}; };
this.AssignRuntimeIdentity(chatThread); this.AssignRuntimeIdentity(chatThread);
var userPrompt = new ContentText var userPrompt = input.Image is null
{ ? new ContentText { Text = BuildUserPrompt(fileName, input.Text) }
Text = BuildUserPrompt(fileName, fileContent), : new ContentText { Text = BuildImageUserPrompt(fileName), FileAttachments = [input.Image] };
};
chatThread.Blocks.Add(new ContentBlock chatThread.Blocks.Add(new ContentBlock
{ {
@@ -1,6 +1,8 @@
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using AIStudio.Settings;
namespace AIStudio.Assistants.BatchProcessing; namespace AIStudio.Assistants.BatchProcessing;
public partial class AssistantBatchProcessing public partial class AssistantBatchProcessing
@@ -42,6 +44,7 @@ public partial class AssistantBatchProcessing
} }
this.PrepareFileResults(resolvedOutputDirectory, files, previousLog, previousResults); this.PrepareFileResults(resolvedOutputDirectory, files, previousLog, previousResults);
await this.WarnAboutUnreadableImagesAsync();
await this.CheckpointAssistantSession(); await this.CheckpointAssistantSession();
await this.RunBatchAsync(resolvedOutputDirectory); await this.RunBatchAsync(resolvedOutputDirectory);
} }
@@ -88,6 +91,27 @@ public partial class AssistantBatchProcessing
} }
} }
/// <summary>
/// Tells the user before the first file when the images of the run will fail, because the
/// selected model cannot read images.
/// </summary>
/// <remarks>
/// Counts only the images which this run still processes. Images done by an earlier run of a
/// continued batch keep their results and do not fail.
/// </remarks>
private async Task WarnAboutUnreadableImagesAsync()
{
var numImages = this.fileResults.Count(fileResult => fileResult.Status is not BatchProcessingFileStatus.DONE && IsImage(fileResult.FilePath));
if (numImages == 0 || this.ProviderSettings.SupportsImageInput())
return;
var warning = numImages == 1
? T("The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual.")
: string.Format(T("The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual."), numImages);
await this.MessageBus.SendWarning(new(Icons.Material.Filled.ImageNotSupported, warning));
}
/// <summary> /// <summary>
/// Processes all documents which are not restored from a previous run. /// Processes all documents which are not restored from a previous run.
/// </summary> /// </summary>
@@ -188,14 +212,14 @@ public partial class AssistantBatchProcessing
/// </remarks> /// </remarks>
private async Task ProcessOneFileAsync(BatchProcessingFileResult fileResult, string resolvedOutputDirectory, CancellationToken token) private async Task ProcessOneFileAsync(BatchProcessingFileResult fileResult, string resolvedOutputDirectory, CancellationToken token)
{ {
var fileContent = await this.LoadInputContentAsync(fileResult, token); var input = await this.LoadInputContentAsync(fileResult, token);
if (fileContent is null) if (input is null)
return; return;
string aiAnswer; string aiAnswer;
try try
{ {
(aiAnswer, fileResult.UsedTools) = await this.CallAIAsync(fileResult.FileName, fileContent, token); (aiAnswer, fileResult.UsedTools) = await this.CallAIAsync(fileResult.FileName, input.Value, token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -217,6 +241,17 @@ public partial class AssistantBatchProcessing
return; return;
} }
//
// The request reads the image anew in every round with the model, e.g., after each tool
// call, and quietly leaves out an image which is gone by then. The answer would be about
// nothing and must not count as a result, so that continuing the run processes the image again:
//
if (input.Value.Image is { Exists: false })
{
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The image was no longer available while the model was asked about it."));
return;
}
if (string.IsNullOrWhiteSpace(aiAnswer)) if (string.IsNullOrWhiteSpace(aiAnswer))
{ {
this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The AI answer was empty.")); this.FinishFileResult(fileResult, BatchProcessingFileStatus.FAILED, T("The AI answer was empty."));
@@ -1,3 +1,4 @@
using AIStudio.Settings;
using AIStudio.Tools.Media; using AIStudio.Tools.Media;
namespace AIStudio.Assistants.BatchProcessing; namespace AIStudio.Assistants.BatchProcessing;
@@ -82,39 +83,18 @@ public partial class AssistantBatchProcessing
private IReadOnlyList<string> FindInputFiles(string resolvedOutputDirectory) private IReadOnlyList<string> FindInputFiles(string resolvedOutputDirectory)
{ {
var selection = new FolderFileSelection(FileNamePatterns.Parse(this.filePatterns), FileExclusions.Parse(this.excludePatterns), this.includeSubdirectories, this.includeHiddenFilesAndFolders, this.maxFileSizeMegabytes); var selection = new FolderFileSelection(FileNamePatterns.Parse(this.filePatterns), FileExclusions.Parse(this.excludePatterns), this.includeSubdirectories, this.includeHiddenFilesAndFolders, this.maxFileSizeMegabytes);
var outputFolder = new BatchProcessingOutputFolder(this.inputDirectory, resolvedOutputDirectory, this.ResolveResultsFileName());
var files = new SortedSet<string>(StringComparer.OrdinalIgnoreCase); var files = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
this.numTooLargeFiles = 0; this.numTooLargeFiles = 0;
var normalizedInputDirectory = TrimDirectorySeparator(Path.GetFullPath(this.inputDirectory));
var normalizedOutputDirectory = TrimDirectorySeparator(Path.GetFullPath(resolvedOutputDirectory));
// When the output folder is a folder of its own, we skip everything
// inside it. When it is the input folder itself, we must not skip the
// whole folder: we would not find any document at all. We then skip
// our own output artifacts instead.
var isOutputSeparateFolder = !string.Equals(normalizedInputDirectory, normalizedOutputDirectory, StringComparison.OrdinalIgnoreCase);
// The separator is essential: without it, an output folder named 'out'
// would also exclude a document named 'output-notes.md':
var outputDirectoryPrefix = normalizedOutputDirectory + Path.DirectorySeparatorChar;
foreach (var file in selection.EnumerateCandidates( foreach (var file in selection.EnumerateCandidates(
this.inputDirectory, this.inputDirectory,
includeDirectory: directory => !isOutputSeparateFolder includeDirectory: outputFolder.Enters,
|| !string.Equals(TrimDirectorySeparator(Path.GetFullPath(directory)), normalizedOutputDirectory, StringComparison.OrdinalIgnoreCase),
reportInaccessibleDirectory: (directory, exception) => reportInaccessibleDirectory: (directory, exception) =>
throw new IOException($"Could not access directory '{directory}'.", exception))) throw new IOException($"Could not access directory '{directory}'.", exception)))
{ {
var normalizedFile = Path.GetFullPath(file); var normalizedFile = Path.GetFullPath(file);
if (!selection.Selects(normalizedFile) || IsTranscriptArtifact(normalizedFile)) if (!selection.Selects(normalizedFile) || IsTranscriptArtifact(normalizedFile) || outputFolder.IsOutputOfRun(normalizedFile))
continue;
if (isOutputSeparateFolder)
{
if (normalizedFile.StartsWith(outputDirectoryPrefix, StringComparison.OrdinalIgnoreCase))
continue;
}
else if (this.IsOwnOutputArtifact(normalizedFile))
continue; continue;
// //
@@ -136,25 +116,6 @@ public partial class AssistantBatchProcessing
return [.. files]; return [.. files];
} }
private static string TrimDirectorySeparator(string path) => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
/// <summary>
/// Checks whether a file is an output artifact of this assistant. We need
/// this when the output folder is the input folder: without it, the results
/// of a previous run would be processed as documents.
/// </summary>
private bool IsOwnOutputArtifact(string filePath)
{
var fileName = Path.GetFileName(filePath);
if (string.Equals(fileName, LOG_FILENAME, StringComparison.OrdinalIgnoreCase))
return true;
if (string.Equals(fileName, this.ResolveResultsFileName(), StringComparison.OrdinalIgnoreCase))
return true;
return fileName.EndsWith(RESULT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
}
/// <summary> /// <summary>
/// Checks for persistent or interrupted media transcript artifacts. They /// Checks for persistent or interrupted media transcript artifacts. They
/// always live beside their source file, independently of the output folder. /// always live beside their source file, independently of the output folder.
@@ -217,6 +178,17 @@ public partial class AssistantBatchProcessing
return null; return null;
} }
//
// A model that cannot read images would get the text of the request alone, without the
// image, and answer about nothing. Such images fail one by one, see LoadImageAsync, while the
// documents next to them are processed. A run of nothing but images does not start at all:
//
if (files.All(IsImage) && !this.ProviderSettings.SupportsImageInput())
{
this.AddInputIssue(T("All selected files are images, but the selected model cannot read images. Select a model with image support."));
return null;
}
try try
{ {
Directory.CreateDirectory(resolvedOutputDirectory); Directory.CreateDirectory(resolvedOutputDirectory);
@@ -15,10 +15,13 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
[Inject] [Inject]
private PandocAvailabilityService PandocAvailability { get; init; } = null!; private PandocAvailabilityService PandocAvailability { get; init; } = null!;
[Inject]
private ModelImageService ModelImageService { get; init; } = null!;
private const string DEFAULT_OUTPUT_DIRECTORY_NAME = "ai-results"; private const string DEFAULT_OUTPUT_DIRECTORY_NAME = "ai-results";
private const string DEFAULT_RESULTS_FILENAME = "batch-results.csv"; private const string DEFAULT_RESULTS_FILENAME = "batch-results.csv";
private const string CSV_EXTENSION = ".csv"; private const string CSV_EXTENSION = ".csv";
private const string RESULT_FILE_SUFFIX = "_result"; internal const string RESULT_FILE_SUFFIX = "_result";
private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
private const char LOG_SEPARATOR = ';'; private const char LOG_SEPARATOR = ';';
@@ -26,7 +29,7 @@ public partial class AssistantBatchProcessing : AssistantBaseCore<SettingsDialog
/// The name of the log file. It is fixed, so that a later batch run finds /// The name of the log file. It is fixed, so that a later batch run finds
/// the log of a previous run and can continue it. /// the log of a previous run and can continue it.
/// </summary> /// </summary>
private const string LOG_FILENAME = "log.csv"; internal const string LOG_FILENAME = "log.csv";
protected override Tools.Components Component => Tools.Components.BATCH_PROCESSING_ASSISTANT; protected override Tools.Components Component => Tools.Components.BATCH_PROCESSING_ASSISTANT;
@@ -0,0 +1,88 @@
namespace AIStudio.Assistants.BatchProcessing;
/// <summary>
/// Tells the search for documents which of the files it finds a batch run wrote itself.
/// </summary>
/// <remarks>
/// A run writes its result files, its log, and its results table directly into the output folder.
/// What the search must leave out therefore depends on where this folder lies. Below the input
/// folder, the search leaves out all of it. When it is the input folder itself, leaving out the
/// whole folder would leave no document at all, so the search leaves out what a run writes, by
/// name. Anywhere else, above the input folder included, the search never comes across the output
/// folder and leaves out nothing.
/// </remarks>
/// <param name="inputDirectory">The folder the run reads its documents from.</param>
/// <param name="outputDirectory">The folder the run writes into.</param>
/// <param name="resultsFileName">The file name of the results table.</param>
internal sealed class BatchProcessingOutputFolder(string inputDirectory, string outputDirectory, string resultsFileName)
{
private readonly string fullOutputDirectory = Path.GetFullPath(outputDirectory);
private readonly bool isInputFolder = IsSameFolder(inputDirectory, outputDirectory);
//
// The same folder with a separator at its end would count as lying below itself:
//
private readonly bool liesBelowInputFolder = !IsSameFolder(inputDirectory, outputDirectory) && PathTools.IsInsideDirectory(inputDirectory, outputDirectory);
/// <summary>
/// Whether the search enters a subfolder of the input folder: every one but the output folder.
/// </summary>
/// <param name="directory">The subfolder.</param>
public bool Enters(string directory) => !this.liesBelowInputFolder || !IsSameFolder(directory, this.fullOutputDirectory);
/// <summary>
/// Whether a file the search found was written by a run instead of being a document.
/// </summary>
/// <param name="filePath">The full path of the file.</param>
public bool IsOutputOfRun(string filePath)
{
//
// Checked with the separator after the name of the folder: otherwise, an output folder
// named 'out' would also leave out a document named 'output-notes.md':
//
if (this.liesBelowInputFolder)
return PathTools.IsInsideDirectory(this.fullOutputDirectory, filePath);
//
// A run writes into the output folder itself, never into one of its subfolders:
//
return this.isInputFolder
&& Path.GetDirectoryName(Path.GetFullPath(filePath)) is { } fileDirectory
&& IsSameFolder(fileDirectory, this.fullOutputDirectory)
&& this.IsOwnOutputArtifact(filePath);
}
/// <summary>
/// Checks whether a file is an output artifact of the assistant. We need
/// this when the output folder is the input folder: without it, the results
/// of a previous run would be processed as documents.
/// </summary>
/// <remarks>
/// A result file is recognized by its name without the extension, e.g., report_result.md or
/// report_result_2.docx, cf. AssistantBatchProcessing.CreateResultFileName. Its extension does
/// not count, because the user may have chosen another file format since a previous run.
/// </remarks>
private bool IsOwnOutputArtifact(string filePath)
{
var fileName = Path.GetFileName(filePath);
if (string.Equals(fileName, AssistantBatchProcessing.LOG_FILENAME, StringComparison.OrdinalIgnoreCase)
|| string.Equals(fileName, resultsFileName, StringComparison.OrdinalIgnoreCase))
return true;
var stem = Path.GetFileNameWithoutExtension(fileName);
var suffixIndex = stem.LastIndexOf(AssistantBatchProcessing.RESULT_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase);
if (suffixIndex < 0)
return false;
//
// A second result of the same name gets a counter, e.g., report_result_2.md:
//
var counter = stem[(suffixIndex + AssistantBatchProcessing.RESULT_FILE_SUFFIX.Length)..];
return counter.Length is 0 || (counter.Length > 1 && counter[0] is '_' && counter[1..].All(char.IsAsciiDigit));
}
private static bool IsSameFolder(string firstDirectory, string secondDirectory) => string.Equals(TrimDirectorySeparator(Path.GetFullPath(firstDirectory)), TrimDirectorySeparator(Path.GetFullPath(secondDirectory)), PathTools.COMPARISON);
private static string TrimDirectorySeparator(string path) => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
@@ -1,7 +1,7 @@
@attribute [Route(Routes.ASSISTANT_META_ASSISTANT)] @attribute [Route(Routes.ASSISTANT_META_ASSISTANT)]
@using AIStudio.Agents.AssistantAudit @using AIStudio.Agents.AssistantAudit
@using AIStudio.Tools.PluginSystem.Assistants.DataModel @using AIStudio.Tools.PluginSystem.Assistants.DataModel
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.NoSettingsPanel> @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBuilder>
<PreviewBeta ApplyInnerScrollingFix="true"/> <PreviewBeta ApplyInnerScrollingFix="true"/>
@@ -11,7 +11,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants.Builder; namespace AIStudio.Assistants.Builder;
public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel> public partial class AssistantBuilder : AssistantBaseCore<SettingsDialogAssistantBuilder>
{ {
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@@ -82,8 +82,6 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
// The result is the Lua code of a plugin, and a notice below it would make that code invalid: // The result is the Lua code of a plugin, and a notice below it would make that code invalid:
protected override bool AddsAIGeneratedContentDisclosure => false; protected override bool AddsAIGeneratedContentDisclosure => false;
protected override bool HasSettingsPanel => false;
protected override Func<string> Result2Copy => () => !string.IsNullOrWhiteSpace(this.generatedLuaAssistant) ? this.generatedLuaAssistant : this.generatedAssistantSpec; protected override Func<string> Result2Copy => () => !string.IsNullOrWhiteSpace(this.generatedLuaAssistant) ? this.generatedLuaAssistant : this.generatedAssistantSpec;
private BuilderStep step = BuilderStep.DESCRIBE; private BuilderStep step = BuilderStep.DESCRIBE;
@@ -251,8 +249,6 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
this.launcherDataSourceIds = []; this.launcherDataSourceIds = [];
this.launcherToolIds = []; this.launcherToolIds = [];
this.selectedAssistantComponents = []; this.selectedAssistantComponents = [];
this.selectedOutputLanguage = CommonLanguages.AS_IS;
this.customOutputLanguage = string.Empty;
this.allowGeneratedAssistantProfiles = true; this.allowGeneratedAssistantProfiles = true;
this.extraRules = string.Empty; this.extraRules = string.Empty;
this.exampleRequest = string.Empty; this.exampleRequest = string.Empty;
@@ -260,9 +256,25 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
this.reviewNotes = string.Empty; this.reviewNotes = string.Empty;
this.generatedLuaAssistant = string.Empty; this.generatedLuaAssistant = string.Empty;
this.ResetInstallFlow(); this.ResetInstallFlow();
if (!this.MightPreselectValues())
{
this.selectedOutputLanguage = CommonLanguages.AS_IS;
this.customOutputLanguage = string.Empty;
}
} }
protected override bool MightPreselectValues() => false; protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions)
{
this.selectedOutputLanguage = this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage;
this.customOutputLanguage = this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage;
return true;
}
return false;
}
/// <inheritdoc /> /// <inheritdoc />
protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state) protected override void CaptureCustomAssistantSessionState(AssistantSessionStateWriter state)
@@ -424,6 +424,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The selected folder does not exist. -- The selected folder does not exist.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "The selected folder does not exist." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "The selected folder does not exist."
-- The image was no longer available while the model was asked about it.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1805791865"] = "The image was no longer available while the model was asked about it."
-- Minimum pause between files -- Minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Minimum pause between files" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Minimum pause between files"
@@ -529,9 +532,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Was not able to write the result file: {0} -- Was not able to write the result file: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}"
-- Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3011193639"] = "Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start."
-- The batch run finished, but one file could not be processed. See the progress table and log for details. -- The batch run finished, but one file could not be processed. See the progress table and log for details.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details."
-- The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T324865195"] = "The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual."
-- Maximum pause between files -- Maximum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximum pause between files" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximum pause between files"
@@ -550,6 +559,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Show Expert Settings -- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Show Expert Settings" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Show Expert Settings"
-- All selected files are images, but the selected model cannot read images. Select a model with image support.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3365102967"] = "All selected files are images, but the selected model cannot read images. Select a model with image support."
-- Header of the result column (optional) -- Header of the result column (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Header of the result column (optional)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Header of the result column (optional)"
@@ -562,6 +574,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'. -- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'."
-- The selected model cannot read images. Select a model with image support to process this image.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T346986514"] = "The selected model cannot read images. Select a model with image support to process this image."
-- Document analysis policy -- Document analysis policy
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Document analysis policy" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Document analysis policy"
@@ -676,6 +691,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here. -- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here."
-- The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T9738678"] = "The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual."
-- Comma (,) -- Comma (,)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Comma (,)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Comma (,)"
@@ -4120,9 +4138,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Yo
-- Automatic local data source refresh -- Automatic local data source refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh"
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1215599168"] = "Edit Local Directory Data Source"
-- Refresh -- Refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh"
@@ -4150,12 +4165,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "Ar
-- Local Directory Data Source Information -- Local Directory Data Source Information
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T221059217"] = "Edit ERI v1 Data Source"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2453292893"] = "Edit Local File Data Source"
-- Local data sources refresh when files change, mailboxes every 16 minutes. -- Local data sources refresh when files change, mailboxes every 16 minutes.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes."
@@ -4390,9 +4399,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = "
-- Workspace name (Optional) -- Workspace name (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)"
-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats."
-- Use no profile -- Use no profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile"
-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."
-- Existing workspace (Optional) -- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)"
@@ -4414,6 +4429,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = "
-- Data sources (Optional) -- Data sources (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)"
-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists."
-- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings."
@@ -6178,12 +6196,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. -- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options."
-- Preselected provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Preselected provider"
-- Please enter a name for the chat template. -- Please enter a name for the chat template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats."
-- Yes, this template decides which data a chat starts with -- Yes, this template decides which data a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with"
-- Preselected profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Preselected profile"
-- Enter the full path of a file on this computer, or remove the attachment. -- Enter the full path of a file on this computer, or remove the attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment."
@@ -6193,12 +6220,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load pr
-- Update -- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update"
-- Yes, this template decides which profile a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Yes, this template decides which profile a chat starts with"
-- The chat template name must not exceed 40 characters. -- The chat template name must not exceed 40 characters.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters."
-- Profile Usage -- Profile Usage
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage"
-- Use no profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Use no profile"
-- Yes, chats with this template go into a workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Yes, chats with this template go into a workspace"
-- Relink attachment: {0} -- Relink attachment: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}"
@@ -6208,6 +6244,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add mes
-- File Attachments -- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments"
-- No, chats go where they would go anyway
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "No, chats go where they would go anyway"
-- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Existing workspace (Optional)"
-- Role -- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
@@ -6220,6 +6262,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools"
-- What predefined user input do you want to use? -- What predefined user input do you want to use?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?"
-- Please enter the workspace for the chats of this template, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Please enter the workspace for the chats of this template, or switch the preselection off."
-- Does this chat template preselect a provider?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Does this chat template preselect a provider?"
-- Tell the AI your system prompt. -- Tell the AI your system prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt."
@@ -6229,6 +6277,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro
-- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with."
-- Does this chat template preselect a profile?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Does this chat template preselect a profile?"
-- Predefined User Input -- Predefined User Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input"
@@ -6244,9 +6295,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "The mes
-- Chat Template Name -- Chat Template Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name"
-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on."
-- Please relink the attachment '{0}' to a file on this computer or remove it before you save. -- Please relink the attachment '{0}' to a file on this computer or remove it before you save.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save."
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Workspace name"
-- Commit Changes -- Commit Changes
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes"
@@ -6262,21 +6319,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats."
-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat."
-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile."
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here."
-- Choose an existing workspace or enter a name that should be created when a chat with this template starts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Choose an existing workspace or enter a name that should be created when a chat with this template starts."
-- No, chats keep the data source options from your chat options -- No, chats keep the data source options from your chat options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options"
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. -- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace."
-- Add a message -- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
-- Yes, this template decides which provider a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Yes, this template decides which provider a chat starts with"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- No, chats keep their usual profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "No, chats keep their usual profile"
-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider."
-- Unsupported content type -- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
@@ -6313,12 +6391,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the
-- Tell the AI your predefined user input. -- Tell the AI your predefined user input.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input."
-- Does this chat template keep its chats in a workspace?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Does this chat template keep its chats in a workspace?"
-- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template."
-- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements."
-- Please choose the provider this chat template preselects, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Please choose the provider this chat template preselects, or switch the preselection off."
-- Enter a message -- Enter a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
@@ -6331,15 +6415,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. -- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
-- Workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Workspace"
-- Remove this attachment -- Remove this attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment"
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. -- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
-- No, chats keep their usual provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "No, chats keep their usual provider"
-- Allow the use of profiles together with this chat template? -- Allow the use of profiles together with this chat template?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Provider"
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
@@ -6805,9 +6898,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2460883298"
-- Add -- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Add" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Add"
-- The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2686150329"] = "The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed."
-- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud. -- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "The embedding you selected runs locally or in your organization. Your data is not sent to the cloud." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "The embedding you selected runs locally or in your organization. Your data is not sent to the cloud."
@@ -7066,9 +7156,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T787118522"] = "T
-- Data Source Name -- Data Source Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Data Source Name" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Data Source Name"
-- The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T890383764"] = "The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed."
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Cancel"
@@ -8578,6 +8665,33 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T6790
-- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model. -- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model."
-- When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T1496952909"] = "When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language."
-- Preselect Assistant Builder options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T2127606148"] = "Preselect Assistant Builder options?"
-- Default custom output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3360584376"] = "Default custom output language"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3448155331"] = "Close"
-- Default output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3652922567"] = "Default output language"
-- Assistant: Assistant Builder defaults
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3668274570"] = "Assistant: Assistant Builder defaults"
-- Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3962364100"] = "Assistant Builder options are preselected"
-- No Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T4104323209"] = "No Assistant Builder options are preselected"
-- The language in which the assistants you build answer.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T42126731"] = "The language in which the assistants you build answer."
-- Default minimum pause between files -- Default minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Default minimum pause between files" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Default minimum pause between files"
@@ -8623,6 +8737,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T22
-- Hidden files and folders are included -- Hidden files and folders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Hidden files and folders are included" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Hidden files and folders are included"
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T232588799"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format."
-- The lower end of the random pause interval. AI Studio never allows less than 6 seconds. -- The lower end of the random pause interval. AI Studio never allows less than 6 seconds.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "The lower end of the random pause interval. AI Studio never allows less than 6 seconds." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "The lower end of the random pause interval. AI Studio never allows less than 6 seconds."
@@ -8635,9 +8752,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T24
-- When enabled, new batch runs start with the defaults configured below. -- When enabled, new batch runs start with the defaults configured below.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below."
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2594325620"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats."
-- Subfolders are included -- Subfolders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Subfolders are included" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Subfolders are included"
@@ -10372,6 +10486,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Failed mails: {0}"
-- Skipped -- Skipped
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped"
-- Edit this data source
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T878700886"] = "Edit this data source"
-- Open the transcription settings -- Open the transcription settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings"
@@ -12208,18 +12325,39 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEM
-- Qdrant Edge is not available. -- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available."
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T1215599168"] = "Edit Local Directory Data Source"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T221059217"] = "Edit ERI v1 Data Source"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T2453292893"] = "Edit Local File Data Source"
-- Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T1555760974"] = "Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are."
-- AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2205693550"] = "AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in."
-- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one. -- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one."
-- and {0} more. -- and {0} more.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "and {0} more." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "and {0} more."
-- Your embedding provider runs in the cloud, so preparing files again costs money.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2635623258"] = "Your embedding provider runs in the cloud, so preparing files again costs money."
-- This change makes the prepared documents of the following data sources unusable ({0}): -- This change makes the prepared documents of the following data sources unusable ({0}):
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "This change makes the prepared documents of the following data sources unusable ({0}):" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "This change makes the prepared documents of the following data sources unusable ({0}):"
-- Do you want to apply this change anyway? -- Do you want to apply this change anyway?
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Do you want to apply this change anyway?" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Do you want to apply this change anyway?"
-- New Location for This Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3462449568"] = "New Location for This Data Source"
-- Documents Will Be Prepared Again -- Documents Will Be Prepared Again
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Documents Will Be Prepared Again" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Documents Will Be Prepared Again"
@@ -12967,6 +13105,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
-- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured."
-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead."
-- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself."
@@ -12979,6 +13120,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T260
-- The chat template preselects tools which are not available on this computer: {0}. -- The chat template preselects tools which are not available on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}."
-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead."
-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead."
-- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider."
@@ -13000,6 +13147,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600
-- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider."
-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile."
-- Expected a quoted string at character {0}. -- Expected a quoted string at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}."
@@ -13402,9 +13552,36 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like p
-- Document -- Document
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
-- Image for AI models
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T791951937"] = "Image for AI models"
-- Plugin archive -- Plugin archive
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive"
-- AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1345506748"] = "AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent."
-- The image '{0}' could not be made small enough for the selected provider, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1463083567"] = "The image '{0}' could not be made small enough for the selected provider, so it was not sent."
-- The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2022724454"] = "The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable."
-- The image '{0}' does not exist anymore and was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2330229240"] = "The image '{0}' does not exist anymore and was not sent."
-- The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T3457568536"] = "The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension."
-- The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T4029166607"] = "The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely."
-- The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T567941882"] = "The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first."
-- The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T568830141"] = "The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG."
-- Attempt to override instructions -- Attempt to override instructions
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Attempt to override instructions" UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Attempt to override instructions"
@@ -13759,18 +13936,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T8042
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1393648320"] = "The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source."
-- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run. -- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2268993048"] = "The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2692299965"] = "The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source."
-- The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2999277901"] = "The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source."
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3722338131"] = "The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source."
-- The file '{0}' could not be read. Please check whether you are allowed to read it. -- The file '{0}' could not be read. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "The file '{0}' could not be read. Please check whether you are allowed to read it." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "The file '{0}' could not be read. Please check whether you are allowed to read it."
@@ -14632,11 +14812,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"]
-- Please enter the secret necessary for authentication. -- Please enter the secret necessary for authentication.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication." UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication."
-- Unsupported image format -- AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1398282880"] = "Unsupported image format" UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1578559061"] = "AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG."
-- File has no extension
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1555980031"] = "File has no extension"
-- Audio files are not supported yet -- Audio files are not supported yet
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio files are not supported yet" UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio files are not supported yet"
@@ -1,7 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Tools.Validation;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@@ -53,23 +52,6 @@ public sealed class ContentImage : IContent, IImageSource
#endregion #endregion
/// <summary>
/// Creates a ContentImage from a local file path.
/// </summary>
/// <param name="filePath">The path to the image file.</param>
/// <returns>A new ContentImage instance if the file is valid, null otherwise.</returns>
public static async Task<ContentImage?> CreateFromFileAsync(string filePath)
{
if (!await FileExtensionValidation.IsImageExtensionValidWithNotifyAsync(filePath))
return null;
return new ContentImage
{
SourceType = ContentImageSource.LOCAL_PATH,
Source = filePath,
};
}
/// <summary> /// <summary>
/// The type of the image source. /// The type of the image source.
/// </summary> /// </summary>
@@ -1,4 +1,5 @@
using AIStudio.Tools.MIME; using AIStudio.Provider;
using AIStudio.Provider.SelfHosted;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@@ -7,72 +8,40 @@ public static class IImageSourceExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IImageSourceExtensions).Namespace, nameof(IImageSourceExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IImageSourceExtensions).Namespace, nameof(IImageSourceExtensions));
public static MIMEType DetermineMimeType(this IImageSource image) /// <summary>
{ /// The largest image, in bytes, we send anywhere: no image prepared for a model is larger, and
switch (image.SourceType) /// TryAsBase64 skips every larger file.
{ /// </summary>
case ContentImageSource.BASE64: public const long MAX_IMAGE_SIZE_BYTES = 10_000_000;
{
// Try to detect the mime type from the base64 string:
var base64Data = image.Source;
if (base64Data.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
var mimeEnd = base64Data.IndexOf(';');
if (mimeEnd > 5)
return Builder.FromTextRepresentation(base64Data[5..mimeEnd]);
}
// Fallback: /// <summary>
return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build(); /// The largest image file, in bytes, which we send through the given provider.
} /// </summary>
/// <remarks>
case ContentImageSource.URL: /// The smallest of the limits we know: the one of a self-hosted engine, the one of the provider,
/// and MAX_IMAGE_SIZE_BYTES, our own. A provider or engine which states no limit gets ours.
/// </remarks>
/// <param name="provider">The configured provider the image is sent through.</param>
/// <returns>The size limit of one image file.</returns>
public static long MaxImageSizeBytesFor(AIStudio.Settings.Provider provider)
{ {
// Try to detect the mime type from the URL extension: var engineLimit = provider.UsedLLMProvider is LLMProviders.SELF_HOSTED ? provider.Host.GetMaxImageSizeBytes() : null;
var uri = new Uri(image.Source); var providerLimit = provider.UsedLLMProvider.GetMaxImageSizeBytes();
var extension = Path.GetExtension(uri.AbsolutePath).ToLowerInvariant(); return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES);
return DeriveMIMETypeFromExtension(extension);
}
case ContentImageSource.LOCAL_PATH:
{
var extension = Path.GetExtension(image.Source).ToLowerInvariant();
return DeriveMIMETypeFromExtension(extension);
}
default:
return Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build();
}
}
private static MIMEType DeriveMIMETypeFromExtension(string extension)
{
var imageBuilder = Builder.Create().UseImage();
return extension switch
{
".png" => imageBuilder.UseSubtype(ImageSubtype.PNG).Build(),
".jpg" or ".jpeg" => imageBuilder.UseSubtype(ImageSubtype.JPEG).Build(),
".gif" => imageBuilder.UseSubtype(ImageSubtype.GIF).Build(),
".webp" => imageBuilder.UseSubtype(ImageSubtype.WEBP).Build(),
".tiff" or ".tif" => imageBuilder.UseSubtype(ImageSubtype.TIFF).Build(),
".heic" or ".heif" => imageBuilder.UseSubtype(ImageSubtype.HEIC).Build(),
_ => Builder.Create().UseApplication().UseSubtype(ApplicationSubtype.OCTET_STREAM).Build()
};
} }
/// <summary> /// <summary>
/// Read the image content as a base64 string. /// Read the image content as a base64 string.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is for the images inside the content of a chat, which go to external data sources and
/// agents as they are. Attached images, which go to a model, take ModelImageService instead: it
/// scales them down and encodes them anew within the limit of the provider.<br/>
/// <br/>
/// The images are directly converted to base64 strings. The maximum /// The images are directly converted to base64 strings. The maximum
/// size of the image is around 10 MB. If the image is larger, the method /// size of the image is around 10 MB. If the image is larger, the method
/// returns an empty string.<br/> /// returns an empty string.<br/>
/// <br/> /// <br/>
/// As of now, this method does no sort of image processing. LLMs usually
/// do not work with arbitrary image sizes. In the future, we might have
/// to resize the images before sending them to the model.<br/>
/// <br/>
/// Note as well that this method returns just the base64 string without /// Note as well that this method returns just the base64 string without
/// any data URI prefix (like "data:image/png;base64,"). The caller has /// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed. /// to take care of that if needed.
@@ -97,7 +66,7 @@ public static class IImageSourceExtensions
{ {
// Read the length of the content: // Read the length of the content:
var lengthBytes = response.Content.Headers.ContentLength; var lengthBytes = response.Content.Headers.ContentLength;
if(lengthBytes > 10_000_000) if(lengthBytes > MAX_IMAGE_SIZE_BYTES)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image."))); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image.")));
return (success: false, string.Empty); return (success: false, string.Empty);
@@ -116,7 +85,7 @@ public static class IImageSourceExtensions
{ {
// Read the content length: // Read the content length:
var length = new FileInfo(image.Source).Length; var length = new FileInfo(image.Source).Length;
if(length > 10_000_000) if(length > MAX_IMAGE_SIZE_BYTES)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image."))); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image.")));
return (success: false, string.Empty); return (success: false, string.Empty);
@@ -1,7 +1,9 @@
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Security; using AIStudio.Tools.Security;
using AIStudio.Tools.Services;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@@ -14,14 +16,16 @@ public static class ListContentBlockExtensions
/// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param> /// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param> /// <param name="provider">The configured provider, whose model is being written to.</param>
/// <param name="textSubContentFactory">A factory function to create text sub-content.</param> /// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
/// <param name="imageSubContentFactory">A factory function to create image sub-content.</param> /// <param name="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns> /// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
public static async Task<IList<IMessageBase>> BuildMessagesAsync( public static async Task<IList<IMessageBase>> BuildMessagesAsync(
this List<ContentBlock> blocks, this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider, AIStudio.Settings.Provider provider,
Func<ChatRole, string> roleTransformer, Func<ChatRole, string> roleTransformer,
Func<string, ISubContent> textSubContentFactory, Func<string, ISubContent> textSubContentFactory,
Func<FileAttachmentImage, Task<ISubContent>> imageSubContentFactory) Func<PreparedModelImage, ISubContent> imageSubContentFactory,
CancellationToken token = default)
{ {
// //
// Asked through the configured provider, so that what a person set in their expert settings // Asked through the configured provider, so that what a person set in their expert settings
@@ -59,7 +63,7 @@ public static class ListContentBlockExtensions
// Text prompt with images as attachments, and the provider/model can process images: // Text prompt with images as attachments, and the provider/model can process images:
case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages(): case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages():
messageTaskList.Add(CreateMultimodalMessageAsync(block, text, textSubContentFactory, imageSubContentFactory)); messageTaskList.Add(CreateMultimodalMessageAsync(block, text));
break; break;
} }
} }
@@ -81,34 +85,41 @@ public static class ListContentBlockExtensions
} }
// Local function to create a multimodal message asynchronously. // Local function to create a multimodal message asynchronously.
Task<IMessageBase> CreateMultimodalMessageAsync( Task<IMessageBase> CreateMultimodalMessageAsync(ContentBlock block, ContentText text)
ContentBlock block,
ContentText text,
Func<string, ISubContent> innerTextSubContentFactory,
Func<FileAttachmentImage, Task<ISubContent>> innerImageSubContentFactory)
{ {
return Task.Run(async () => return Task.Run(async () =>
{ {
var imagesTasks = text.FileAttachments var images = await Task.WhenAll(text.FileAttachments
.Where(x => x is { IsImage: true, Exists: true }) .Where(x => x is { IsImage: true, Exists: true })
.Cast<FileAttachmentImage>() .Cast<FileAttachmentImage>()
.Select(innerImageSubContentFactory) .Select(CreateImageSubContentAsync));
.ToList();
Task.WaitAll(imagesTasks);
var images = imagesTasks.Select(t => t.Result).ToList();
return new MultimodalMessage return new MultimodalMessage
{ {
Role = roleTransformer(block.Role), Role = roleTransformer(block.Role),
Content = Content =
[ [
innerTextSubContentFactory(await text.PrepareTextContentForAI(reportingScope)), textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)),
..images, ..images.OfType<ISubContent>(),
] ]
} as IMessageBase; } as IMessageBase;
}); });
} }
//
// Local function to prepare one image for the model. An image which cannot be prepared
// stays out of the message, and the user learns why. It used to go along as an image
// without data, which the providers refuse together with the whole request.
//
async Task<ISubContent?> CreateImageSubContentAsync(FileAttachmentImage image)
{
var prepared = await Program.SERVICE_PROVIDER.GetRequiredService<ModelImageService>().PrepareAsync(image, provider, token);
if (prepared.IsPrepared)
return imageSubContentFactory(prepared);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName)));
return null;
}
} }
/// <summary> /// <summary>
@@ -158,20 +169,16 @@ public static class ListContentBlockExtensions
Text = text, Text = text,
}; };
private static async Task<ISubContent> DirectImageSubContentFactory(FileAttachmentImage attachment) => new SubContentImageUrl private static ISubContent DirectImageSubContentFactory(PreparedModelImage image) => new SubContentImageUrl
{ {
ImageUrl = await attachment.TryAsBase64() is (true, var base64Content) ImageUrl = image.ToDataUrl(),
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
}; };
private static async Task<ISubContent> NestedImageSubContentFactory(FileAttachmentImage attachment) => new SubContentImageUrlNested private static ISubContent NestedImageSubContentFactory(PreparedModelImage image) => new SubContentImageUrlNested
{ {
ImageUrl = new SubContentImageUrlData ImageUrl = new SubContentImageUrlData
{ {
Url = await attachment.TryAsBase64() is (true, var base64Content) Url = image.ToDataUrl(),
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
}, },
}; };
@@ -901,6 +901,84 @@ public partial class ChatComponent : MSGComponentBase
this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds); this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds);
} }
/// <summary>
/// Takes over the profile and provider which the chat template preselects, wherever it preselects one.
/// </summary>
/// <remarks>
/// A preselection, not a limit — the user changes both in the chat as usual. A template which
/// says nothing, or names a provider which is gone or may not be used for chats, leaves the
/// current choice in place, so the chat starts as it would without the template. A template
/// whose profile is gone starts with the default profile of the chat settings instead. What the
/// previous template preselected is not carried over, see SettingsManager.ChooseChatTemplateProfile.
/// The caller tells the parent about a new provider.
/// </remarks>
/// <param name="previousChatTemplate">The chat template the current profile and provider were chosen with; the current one when the template does not change.</param>
/// <returns>True when the provider changed.</returns>
private bool ApplyChatTemplateProfileAndProvider(ChatTemplate previousChatTemplate)
{
this.currentProfile = this.SettingsManager.ChooseChatTemplateProfile(this.currentChatTemplate, previousChatTemplate, this.currentProfile);
var provider = this.SettingsManager.ChooseChatTemplateProvider(this.currentChatTemplate, previousChatTemplate, this.Provider);
if (provider == this.Provider)
return false;
this.Provider = provider;
return true;
}
/// <summary>
/// The workspace a chat which has no thread yet gets with its first content.
/// </summary>
/// <remarks>
/// The first content is the first message, a media import, or a draft kept when the user leaves
/// the page. A chat without a thread belongs to no workspace, so the workspace of its chat
/// template fills in, opened or created by its name. That happens only here, when the chat gets
/// something to keep: merely opening the chat page or trying out a template creates no
/// workspace, and a workspace the user deleted stays gone until a chat with that template
/// actually starts. Without a usable workspace of the template, the chat stays without one.
/// </remarks>
/// <returns>The ID of the workspace, or Guid.Empty for none.</returns>
private async Task<Guid> ResolveWorkspaceOfFirstThreadAsync()
{
if (this.currentWorkspaceId != Guid.Empty)
return this.currentWorkspaceId;
if (this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not { } workspaceName)
return Guid.Empty;
return await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(workspaceName, this.currentChatTemplate.GetSafeName());
}
/// <summary>
/// Hands a thread which this component just created for a chat without one to the parent.
/// </summary>
/// <remarks>
/// The thread is a parameter of this component, and the parent hands over its own whenever it
/// renders. So the parent hears about the new thread before anything else may render it, see
/// StartNewChat. Only afterward, the header shows the workspace of the thread, since showing a
/// workspace name renders the parent as well.
/// </remarks>
private async Task PublishNewChatThreadAsync()
{
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
// The header shows the workspace of the new thread from now on, e.g., the one of its chat template:
if (this.ChatThread is not null && this.ChatThread.WorkspaceId != Guid.Empty)
await this.SyncWorkspaceHeaderWithChatThreadAsync();
}
/// <summary>
/// Whether the open chat is in the workspace which the given chat template names.
/// </summary>
/// <remarks>
/// Such a workspace was the choice of the template rather than of the user, so a chat started
/// with another template does not have to stay there.
/// </remarks>
private bool IsInWorkspaceOfChatTemplate(ChatTemplate chatTemplate) => this.currentWorkspaceId != Guid.Empty
&& this.SettingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } workspaceName
&& string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(this.currentWorkspaceName), workspaceName, StringComparison.OrdinalIgnoreCase);
/// <summary> /// <summary>
/// The data source options a chat starts with: those of its chat template, or the chat defaults. /// The data source options a chat starts with: those of its chat template, or the chat defaults.
/// </summary> /// </summary>
@@ -1006,24 +1084,36 @@ public partial class ChatComponent : MSGComponentBase
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate) private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
{ {
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id); var nextChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
if (this.ChatThread is not null)
{
//
// Starting the new chat is what hands the new template to it, with its text, attachments,
// and selections. Nothing changes here before, since the user may still decline to leave
// the open chat; it then stays as it was, with its template. The new chat stays in the
// workspace of the open one, unless that workspace is merely the one of the current
// template: then the new template decides where its chat goes.
//
await this.StartNewChat(useSameWorkspace: !this.IsInWorkspaceOfChatTemplate(this.currentChatTemplate), chatTemplate: nextChatTemplate);
return;
}
var previousChatTemplate = this.currentChatTemplate;
this.currentChatTemplate = nextChatTemplate;
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt)) if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt); this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
// Apply template's file attachments (replaces existing): // Apply template's file attachments (replaces existing):
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments); this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
if (this.ChatThread is not null)
{
// Starting the new chat is what hands the selection of the new template to it:
await this.StartNewChat(true);
return;
}
// //
// Without a thread there is nothing to start anew, so the selection of the new template is // Without a thread there is nothing to start anew, so the selection of the new template is
// applied right here. It travels into the thread which the first message creates. // applied right here. It travels into the thread which the first message creates, and that
// message also decides the workspace, see ResolveWorkspaceOfFirstThreadAsync.
// //
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
await this.ProviderChanged.InvokeAsync(this.Provider);
await this.ApplyChatTemplateToolSelectionAsync(); await this.ApplyChatTemplateToolSelectionAsync();
this.ApplyAutomaticDataSourceOptions(); this.ApplyAutomaticDataSourceOptions();
} }
@@ -1047,6 +1137,19 @@ public partial class ChatComponent : MSGComponentBase
{ {
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT); this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
//
// A chat which has not started yet keeps up with its chat template. The provider follows
// only when the template now preselects another one, so that a provider the user picked
// by hand survives a change of some unrelated setting:
//
if (previousChatTemplate.Id != this.currentChatTemplate.Id || previousChatTemplate.ProviderId != this.currentChatTemplate.ProviderId)
{
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
await this.ProviderChanged.InvokeAsync(this.Provider);
}
else if (this.SettingsManager.GetChatTemplateStartProfile(this.currentChatTemplate) is { } templateProfile)
this.currentProfile = templateProfile;
} }
else else
{ {
@@ -1256,10 +1359,16 @@ public partial class ChatComponent : MSGComponentBase
if (!this.ComposerState.HasVisibleUserDraft) if (!this.ComposerState.HasVisibleUserDraft)
return; return;
//
// The message moves out of the composer before anything below waits, so that leaving a
// second time meanwhile finds nothing left to keep. Only the workspace follows afterward,
// since finding or creating the one of the chat template may take a moment:
//
this.RefreshCurrentProfileAndChatTemplate(); this.RefreshCurrentProfileAndChatTemplate();
chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput)); chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), Guid.Empty);
this.MirrorComposerIntoDraft(chat); this.MirrorComposerIntoDraft(chat);
this.ComposerState.Clear(); this.ComposerState.Clear();
chat.WorkspaceId = await this.ResolveWorkspaceOfFirstThreadAsync();
} }
else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId)))) else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId))))
{ {
@@ -1330,22 +1439,28 @@ public partial class ChatComponent : MSGComponentBase
? string.Format(T("Transcription: {0}"), Path.GetFileName(firstMediaPath)) ? string.Format(T("Transcription: {0}"), Path.GetFileName(firstMediaPath))
: promptName; : promptName;
this.ChatThread = this.NewChatThread(threadName); var chat = this.NewChatThread(threadName, await this.ResolveWorkspaceOfFirstThreadAsync());
// A message the user started belongs to the new chat, so it is stored with it: // A message the user started belongs to the new chat, so it is stored with it:
if (this.ComposerState.HasUserDraft) if (this.ComposerState.HasUserDraft)
this.MirrorComposerIntoDraft(this.ChatThread); this.MirrorComposerIntoDraft(chat);
//
// The parent hears about the new thread before storing it waits, as with the first message.
// A render of the parent in between would otherwise hand back no thread at all, and the
// transcript would arrive in a chat which is nowhere:
//
this.ChatThread = chat;
await this.PublishNewChatThreadAsync();
// //
// Like saving, this goes through the workspace component whenever there is one, so its tree // Like saving, this goes through the workspace component whenever there is one, so its tree
// lists the new chat right away, together with the progress of the transcription, instead // lists the new chat right away, together with the progress of the transcription, instead
// of only after the first message was sent: // of only after the first message was sent:
// //
await this.StoreAndListChatAsync(this.ChatThread); await this.StoreAndListChatAsync(chat);
this.MarkCurrentChatAsLoadedParameter();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
await this.SyncForegroundChatAsync(); await this.SyncForegroundChatAsync();
return this.ChatThread; return chat;
} }
private async Task SendMessage(bool reuseLastUserPrompt = false) private async Task SendMessage(bool reuseLastUserPrompt = false)
@@ -1377,9 +1492,8 @@ public partial class ChatComponent : MSGComponentBase
// Create a new chat thread if necessary: // Create a new chat thread if necessary:
if (this.ChatThread is null) if (this.ChatThread is null)
{ {
this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput)); this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), await this.ResolveWorkspaceOfFirstThreadAsync());
this.MarkCurrentChatAsLoadedParameter(); await this.PublishNewChatThreadAsync();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
else else
{ {
@@ -1576,12 +1690,17 @@ public partial class ChatComponent : MSGComponentBase
/// Every way to start a new chat leads here: the buttons of the toolbar, a change of the chat /// Every way to start a new chat leads here: the buttons of the toolbar, a change of the chat
/// template, and the workspace tree. So every new chat gets its chat template with the data /// template, and the workspace tree. So every new chat gets its chat template with the data
/// sources, tools, and example conversation, its profile, and its provider in the same way, and /// sources, tools, and example conversation, its profile, and its provider in the same way, and
/// the user is asked about unsaved changes in the same way. /// the user is asked about unsaved changes in the same way.<br/><br/>
///
/// A new chat template takes over only once the user agreed to leave the open chat and that
/// chat is stored or deleted. Declining leaves the open chat as it was, with its template, and
/// the template it runs with stays the one whose preselections the next chat must not carry over.
/// </remarks> /// </remarks>
/// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message.</param> /// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message. A chat which would belong to no workspace either way, while its chat template names one, also gets its thread only with the first message, in the workspace of the template.</param>
/// <param name="deletePreviousChat">Whether to delete the open chat first.</param> /// <param name="deletePreviousChat">Whether to delete the open chat first.</param>
/// <param name="targetWorkspaceId">The workspace for the new chat, when the workspace tree asks for one. When given, the new chat gets a thread in that workspace, whatever useSameWorkspace says.</param> /// <param name="targetWorkspaceId">The workspace for the new chat, when the workspace tree asks for one. When given, the new chat gets a thread in that workspace, whatever useSameWorkspace says.</param>
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null) /// <param name="chatTemplate">The chat template of the new chat, when the user picked another one. Without one, the new chat keeps the template of the open chat.</param>
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null, ChatTemplate? chatTemplate = null)
{ {
// //
// Where the new chat goes is decided right away, when it is asked for. Deleting the open chat // Where the new chat goes is decided right away, when it is asked for. Deleting the open chat
@@ -1589,6 +1708,9 @@ public partial class ChatComponent : MSGComponentBase
// //
var workspaceOfNewChat = targetWorkspaceId ?? this.currentWorkspaceId; var workspaceOfNewChat = targetWorkspaceId ?? this.currentWorkspaceId;
// The template the profile and the provider of the open chat were chosen with:
var previousChatTemplate = this.currentChatTemplate;
// //
// Want the user to manage the chat storage manually? In that case, we have to ask the user // Want the user to manage the chat storage manually? In that case, we have to ask the user
// about possible data loss: // about possible data loss:
@@ -1650,6 +1772,11 @@ public partial class ChatComponent : MSGComponentBase
// //
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.ComposerState.Clear(); this.ComposerState.Clear();
// The open chat is left now, so the new template takes over:
if (chatTemplate is not null)
this.currentChatTemplate = chatTemplate;
this.RefreshCurrentProfileAndChatTemplate(); this.RefreshCurrentProfileAndChatTemplate();
// //
@@ -1673,11 +1800,26 @@ public partial class ChatComponent : MSGComponentBase
break; break;
} }
// What the chat template preselects comes last, so that it wins over the behavior above:
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
await this.ProviderChanged.InvokeAsync(this.Provider);
// //
// Reset the chat thread or create a new one. A target workspace always means a new chat in // Reset the chat thread or create a new one. A target workspace always means a new chat in
// that workspace, no matter what useSameWorkspace says: // that workspace, no matter what useSameWorkspace says:
// //
var startsOutsideWorkspaces = !useSameWorkspace && targetWorkspaceId is null; var startsOutsideWorkspaces = !useSameWorkspace && targetWorkspaceId is null;
//
// The workspace of the chat template fills in only where the new chat would belong to no
// workspace otherwise. A workspace asked for on purpose wins: the one of the workspace list,
// and the open one for the button that starts a new chat in it. Such a chat gets no thread
// here: its first message creates one in that workspace, so that the workspace is created
// only once there is something to keep, see ResolveWorkspaceOfFirstThreadAsync.
//
if (targetWorkspaceId is null && workspaceOfNewChat == Guid.Empty && this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not null)
startsOutsideWorkspaces = true;
if (startsOutsideWorkspaces) if (startsOutsideWorkspaces)
{ {
// //
@@ -1917,6 +2059,10 @@ public partial class ChatComponent : MSGComponentBase
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider); this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider);
// A chat without a thread has not started yet, so it starts with what its chat template preselects:
if (this.ChatThread is null)
this.ApplyChatTemplateProfileAndProvider(this.currentChatTemplate);
await this.ProviderChanged.InvokeAsync(this.Provider); await this.ProviderChanged.InvokeAsync(this.Provider);
// Try to select the profile: // Try to select the profile:
@@ -2054,7 +2200,7 @@ public partial class ChatComponent : MSGComponentBase
// that, and it may even bring an example conversation along -- reporting nothing for all // that, and it may even bring an example conversation along -- reporting nothing for all
// of it would tell a person their window is empty while their first message is not. // of it would tell a person their window is empty while their first message is not.
// //
var thread = this.ChatThread ?? this.NewChatThread(string.Empty); var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId);
var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch); var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch);
provider = this.Provider; provider = this.Provider;
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions); parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
@@ -2153,18 +2299,22 @@ public partial class ChatComponent : MSGComponentBase
/// The thread a new chat starts with, as the selections made so far decide it. /// The thread a new chat starts with, as the selections made so far decide it.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// In one place because three code paths used to write it out, and because the token count has /// The only place which builds a new thread: every way to start a chat comes here, and so does
/// to measure the same thing they build. A count against a thread assembled differently from /// the token count, which has to measure the same thing they build. A count against a thread
/// the one which is then sent would be wrong in exactly the moment a person looks at it: before /// assembled differently from the one which is then sent would be wrong in exactly the moment a
/// they send their first message. /// person looks at it: before they send their first message.
///
/// The workspace is no default of its own but always named by the caller, since that is the one
/// part the ways to start a chat decide differently: StartNewChat decides it right away, and a
/// chat which gets its thread with its first content asks ResolveWorkspaceOfFirstThreadAsync.
/// ///
/// The data source options are those chosen before the chat had a thread. Starting a new chat in /// The data source options are those chosen before the chat had a thread. Starting a new chat in
/// a workspace replaces them right afterward with those its chat template asks for. /// a workspace replaces them right afterward with those its chat template asks for.
/// </remarks> /// </remarks>
/// <param name="name">The name of the thread.</param> /// <param name="name">The name of the thread.</param>
/// <param name="workspaceId">The workspace of the thread. Without one, the thread goes to the workspace of the open chat.</param> /// <param name="workspaceId">The workspace of the thread, or Guid.Empty for none.</param>
/// <returns>The new thread.</returns> /// <returns>The new thread.</returns>
private ChatThread NewChatThread(string name, Guid? workspaceId = null) => new() private ChatThread NewChatThread(string name, Guid workspaceId) => new()
{ {
DataSourceOptions = this.earlyDataSourceOptions, DataSourceOptions = this.earlyDataSourceOptions,
IncludeDateTime = true, IncludeDateTime = true,
@@ -2173,7 +2323,7 @@ public partial class ChatComponent : MSGComponentBase
SelectedChatTemplate = this.currentChatTemplate.Id, SelectedChatTemplate = this.currentChatTemplate.Id,
SelectedToolIds = [..this.selectedToolIds], SelectedToolIds = [..this.selectedToolIds],
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = workspaceId ?? this.currentWorkspaceId, WorkspaceId = workspaceId,
ChatId = Guid.NewGuid(), ChatId = Guid.NewGuid(),
Name = name, Name = name,
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(), Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(),
@@ -270,69 +270,7 @@ public partial class DataSourceManagement : MSGComponentBase
private async Task EditDataSource(IDataSource dataSource) private async Task EditDataSource(IDataSource dataSource)
{ {
if (dataSource.IsEnterpriseConfiguration) if (await DataSourceEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, dataSource.Id))
return;
IDataSource? editedDataSource = null;
var lockDataSourceOrigin = dataSource is IInternalDataSource
&& await this.DataSourceEmbeddingService.ShouldLockDataSourceOriginAsync(dataSource.Id);
switch (dataSource)
{
case DataSourceLocalFile localFile:
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, localFile },
{ x => x.LockSource, lockDataSourceOrigin },
};
var localFileDialogReference = await this.DialogService.ShowAsync<DataSourceLocalFileDialog>(T("Edit Local File Data Source"), localFileDialogParameters, DialogOptions.FULLSCREEN);
var localFileDialogResult = await localFileDialogReference.Result;
if (localFileDialogResult is null || localFileDialogResult.Canceled)
return;
editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!;
break;
case DataSourceLocalDirectory localDirectory:
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, localDirectory },
{ x => x.LockSource, lockDataSourceOrigin },
};
var localDirectoryDialogReference = await this.DialogService.ShowAsync<DataSourceLocalDirectoryDialog>(T("Edit Local Directory Data Source"), localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
return;
editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
break;
case DataSourceERI_V1 eriDataSource:
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, eriDataSource },
};
var eriDialogReference = await this.DialogService.ShowAsync<DataSourceERI_V1Dialog>(T("Edit ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return;
editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
break;
}
if(editedDataSource is null)
return;
this.SettingsManager.ConfigurationData.DataSources[this.SettingsManager.ConfigurationData.DataSources.IndexOf(dataSource)] = editedDataSource;
await this.SettingsManager.StoreSettings();
await this.DataSourceEmbeddingService.QueueDataSourceAsync(editedDataSource);
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
@@ -1,16 +1,7 @@
@using AIStudio.Settings.DataModel
@inherits MSGComponentBase @inherits MSGComponentBase
@if (this.availableWorkspaces.Count > 0) <WorkspaceNameField WorkspaceName="@this.WorkspaceName" WorkspaceNameChanged="@this.SetWorkspaceName" SelectLabel="@T("Existing workspace (Optional)")" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" AdornmentIconSize="Size.Small" TextFieldClass="mb-1"/>
{
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@T("Existing workspace (Optional)")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
@foreach (var workspace in this.availableWorkspaces)
{
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
}
</MudSelect>
}
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-1" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
@* The tile behaves differently with and without a workspace, so the form says which of the two is @* The tile behaves differently with and without a workspace, so the form says which of the two is
chosen right now instead of only explaining that both are possible. *@ chosen right now instead of only explaining that both are possible. *@
@@ -19,6 +10,12 @@
? T("Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.") ? T("Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.")
: T("The tile opens its chat in this workspace and creates the workspace when it does not exist yet.")) : T("The tile opens its chat in this workspace and creates the workspace when it does not exist yet."))
</MudText> </MudText>
@if (this.OpensTemporaryChat && this.SettingsManager.GetChatTemplateWorkspaceName(this.SelectedChatTemplate) is { } templateWorkspaceName)
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
@string.Format(T("The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."), templateWorkspaceName)
</MudText>
}
<MudSelect T="string" Value="@this.ProviderId" ValueChanged="@this.SetProviderId" Label="@T("Chat provider")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy"> <MudSelect T="string" Value="@this.ProviderId" ValueChanged="@this.SetProviderId" Label="@T("Chat provider")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy">
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem> <MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
@foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT)) @foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT))
@@ -28,6 +25,12 @@
</MudSelectItem> </MudSelectItem>
} }
</MudSelect> </MudSelect>
@if (this.SelectedChatTemplate.ProviderId is not null)
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
@T("The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.")
</MudText>
}
<MudSelect T="string" Value="@this.ProfileId" ValueChanged="@this.SetProfileId" Label="@T("Chat profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person"> <MudSelect T="string" Value="@this.ProfileId" ValueChanged="@this.SetProfileId" Label="@T("Chat profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem> <MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no profile")</MudSelectItem> <MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no profile")</MudSelectItem>
@@ -36,6 +39,12 @@
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem> <MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
} }
</MudSelect> </MudSelect>
@if (this.SelectedChatTemplate is { AllowProfileUsage: true, ProfileId: not null })
{
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
@T("The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.")
</MudText>
}
<MudSelect T="string" Value="@this.ChatTemplateId" ValueChanged="@this.SetChatTemplateId" Label="@T("Chat template")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Article"> <MudSelect T="string" Value="@this.ChatTemplateId" ValueChanged="@this.SetChatTemplateId" Label="@T("Chat template")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Article">
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem> <MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no chat template")</MudSelectItem> <MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no chat template")</MudSelectItem>
@@ -44,6 +53,10 @@
<MudSelectItem T="string" Value="@chatTemplate.Id">@chatTemplate.GetSafeName()</MudSelectItem> <MudSelectItem T="string" Value="@chatTemplate.Id">@chatTemplate.GetSafeName()</MudSelectItem>
} }
</MudSelect> </MudSelect>
@* Without the RAG preview, the chat uses no data sources, so there is nothing to choose here. A
selection made before stays with the launcher and counts once RAG is switched on again: *@
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE"> <MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE">
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources) @foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
{ {
@@ -59,6 +72,7 @@
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.") @T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
</MudText> </MudText>
} }
}
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/> <ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
@@ -96,37 +96,6 @@ public partial class DirectChatLauncherForm : MSGComponentBase
? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT) ? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT)
: this.SettingsManager.GetChatTemplateById(this.ChatTemplateId); : this.SettingsManager.GetChatTemplateById(this.ChatTemplateId);
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
#region Overrides of MSGComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the workspace name input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
await base.OnInitializedAsync();
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
this.availableWorkspaces = workspaceSnapshot.Workspaces;
}
#endregion
//
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
// the user typed, though, so an empty selection is ignored:
//
private async Task SelectExistingWorkspace(string workspaceName)
{
if (string.IsNullOrWhiteSpace(workspaceName))
return;
await this.SetWorkspaceName(workspaceName);
}
private async Task SetWorkspaceName(string workspaceName) private async Task SetWorkspaceName(string workspaceName)
{ {
this.WorkspaceName = workspaceName; this.WorkspaceName = workspaceName;
@@ -0,0 +1,13 @@
@inherits MSGComponentBase
@if (this.availableWorkspaces.Count > 0 && !this.ReadOnly)
{
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@this.SelectLabel" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.SelectClass" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
@foreach (var workspace in this.availableWorkspaces)
{
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
}
</MudSelect>
}
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" Label="@this.Label" HelperText="@this.HelperText" Validation="@this.Validation" Immediate="@this.Immediate" ReadOnly="@this.ReadOnly" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.TextFieldClass" Adornment="Adornment.Start" AdornmentIcon="@this.AdornmentIcon" IconSize="@this.AdornmentIconSize" UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
@@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
/// <summary>
/// Names a workspace by its name: an existing one to pick from, or a new name to type.
/// </summary>
/// <remarks>
/// Assistant chat launchers and chat templates both name their workspace this way, because a
/// workspace has an ID of its own on every computer, and both create it when it does not exist
/// yet. The field offers the existing workspaces only as a shortcut; the name it hands out is
/// whatever the text field says. All texts come from the form around it, which knows what the
/// name is used for.
/// </remarks>
public partial class WorkspaceNameField : MSGComponentBase
{
/// <summary>
/// The name of the workspace. An empty name is up to the form around the field, e.g., a
/// disappearing chat for an assistant chat launcher.
/// </summary>
[Parameter]
public string WorkspaceName { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> WorkspaceNameChanged { get; set; }
/// <summary>
/// The label of the selection of existing workspaces.
/// </summary>
[Parameter]
public string SelectLabel { get; set; } = string.Empty;
/// <summary>
/// The label of the text field for the name.
/// </summary>
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string HelperText { get; set; } = string.Empty;
[Parameter]
public string AdornmentIcon { get; set; } = Icons.Material.Filled.CreateNewFolder;
[Parameter]
public Size AdornmentIconSize { get; set; } = Size.Medium;
/// <summary>
/// The validation of the name, or null for none.
/// </summary>
[Parameter]
public Func<string, string?>? Validation { get; set; }
/// <summary>
/// Whether every keystroke counts as a change, e.g., so that a validation follows the typing.
/// </summary>
[Parameter]
public bool Immediate { get; set; }
/// <summary>
/// Shows the name without letting the user change it. The selection of existing workspaces
/// is left out then, since picking from it would change the name.
/// </summary>
[Parameter]
public bool ReadOnly { get; set; }
[Parameter]
public string SelectClass { get; set; } = "mb-3 rounded-lg";
[Parameter]
public string TextFieldClass { get; set; } = "mb-3";
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
#region Overrides of MSGComponentBase
protected override async Task OnInitializedAsync()
{
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
await base.OnInitializedAsync();
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
this.availableWorkspaces = workspaceSnapshot.Workspaces;
}
#endregion
//
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
// the user typed, though, so an empty selection is ignored:
//
private async Task SelectExistingWorkspace(string workspaceName)
{
if (string.IsNullOrWhiteSpace(workspaceName))
return;
await this.SetWorkspaceName(workspaceName);
}
private async Task SetWorkspaceName(string workspaceName)
{
this.WorkspaceName = workspaceName;
await this.WorkspaceNameChanged.InvokeAsync(workspaceName);
}
}
@@ -1,4 +1,5 @@
@using AIStudio.Chat @using AIStudio.Chat
@using AIStudio.Settings
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@inherits MSGComponentBase @inherits MSGComponentBase
@@ -136,6 +137,73 @@
@T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.") @T("Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.")
</MudJustifiedText> </MudJustifiedText>
<MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" /> <MudTextSwitch @bind-Value="@this.AllowProfileUsage" Color="Color.Primary" Label="@T("Allow the use of profiles together with this chat template?")" LabelOn="@T("Yes, allow profiles when using this template")" LabelOff="@T("No, prohibit profile use for this template")" Disabled="@this.IsReadOnly" />
@if (this.AllowProfileUsage)
{
<MudJustifiedText Class="mb-3 mt-3" Typo="Typo.body1">
@T("A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.")
</MudJustifiedText>
<MudTextSwitch @bind-Value="@this.preselectProfile" Color="Color.Primary" Label="@T("Does this chat template preselect a profile?")" LabelOn="@T("Yes, this template decides which profile a chat starts with")" LabelOff="@T("No, chats keep their usual profile")" Disabled="@this.IsReadOnly"/>
@if (this.preselectProfile)
{
<MudSelect T="string" @bind-Value="@this.selectedProfileId" Label="@T("Preselected profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 mt-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person" Disabled="@this.IsReadOnly">
<MudSelectItem T="string" Value="@Profile.NO_PROFILE.Id">@T("Use no profile")</MudSelectItem>
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
}
</MudSelect>
@if (this.IsPreselectedProfileMissing)
{
<MudAlert Severity="Severity.Warning" Class="mb-3">
@T("The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.")
</MudAlert>
}
}
}
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Provider")
</MudText>
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.")
</MudJustifiedText>
<MudTextSwitch @bind-Value="@this.preselectProvider" Color="Color.Primary" Label="@T("Does this chat template preselect a provider?")" LabelOn="@T("Yes, this template decides which provider a chat starts with")" LabelOff="@T("No, chats keep their usual provider")" Disabled="@this.IsReadOnly"/>
@if (this.preselectProvider)
{
<MudSelect T="string" @bind-Value="@this.selectedProviderId" Label="@T("Preselected provider")" Validation="@this.ValidatePreselectedProvider" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 mt-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy" Disabled="@this.IsReadOnly">
@foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT))
{
<MudSelectItem T="string" Value="@provider.Id">
<ProviderLabel ProviderSettings="@provider" Text="@provider.InstanceName"/>
</MudSelectItem>
}
</MudSelect>
@if (this.IsPreselectedProviderUnusable)
{
<MudAlert Severity="Severity.Warning" Class="mb-3">
@T("The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.")
</MudAlert>
}
}
<MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Workspace")
</MudText>
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.")
</MudJustifiedText>
@* While workspaces are off, nothing goes into a workspace, so choosing one here would promise what no chat does: *@
@if (this.AreWorkspacesDisabled)
{
<MudAlert Severity="Severity.Warning" Class="mb-3">
@T("Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.")
</MudAlert>
}
<MudTextSwitch @bind-Value="@this.preselectWorkspace" Color="Color.Primary" Label="@T("Does this chat template keep its chats in a workspace?")" LabelOn="@T("Yes, chats with this template go into a workspace")" LabelOff="@T("No, chats go where they would go anyway")" Disabled="@this.IsWorkspaceChoiceLocked"/>
@if (this.preselectWorkspace)
{
<WorkspaceNameField @bind-WorkspaceName="@this.selectedWorkspaceName" SelectLabel="@T("Existing workspace (Optional)")" Label="@T("Workspace name")" HelperText="@T("Choose an existing workspace or enter a name that should be created when a chat with this template starts.")" Validation="@this.ValidateWorkspaceName" Immediate="@true" ReadOnly="@this.IsWorkspaceChoiceLocked" SelectClass="mb-3 mt-3 rounded-lg" TextFieldClass="mb-3 mt-3"/>
}
<MudText Typo="Typo.h6" Class="mb-3 mt-6"> <MudText Typo="Typo.h6" Class="mb-3 mt-6">
@T("Tools") @T("Tools")
@@ -79,6 +79,25 @@ public partial class ChatTemplateDialog : MSGComponentBase
[Parameter] [Parameter]
public DataSourceOptions? DataSourceOptions { get; set; } public DataSourceOptions? DataSourceOptions { get; set; }
/// <summary>
/// The profile this template preselects, the ID of the no-profile entry to start without one,
/// or null when it says nothing about profiles.
/// </summary>
[Parameter]
public string? ProfileId { get; set; }
/// <summary>
/// The provider this template preselects, or null when it says nothing about providers.
/// </summary>
[Parameter]
public string? ProviderId { get; set; }
/// <summary>
/// The name of the workspace this template keeps its chats in, or null when it says nothing about workspaces.
/// </summary>
[Parameter]
public string? WorkspaceName { get; set; }
[Parameter] [Parameter]
public bool CreateFromExistingChatThread { get; set; } public bool CreateFromExistingChatThread { get; set; }
@@ -105,6 +124,12 @@ public partial class ChatTemplateDialog : MSGComponentBase
private HashSet<string> selectedToolIds = new(StringComparer.Ordinal); private HashSet<string> selectedToolIds = new(StringComparer.Ordinal);
private bool preselectDataSources; private bool preselectDataSources;
private DataSourceOptions templateDataSourceOptions = new(); private DataSourceOptions templateDataSourceOptions = new();
private bool preselectProfile;
private string selectedProfileId = Profile.NO_PROFILE.Id;
private bool preselectProvider;
private string selectedProviderId = string.Empty;
private bool preselectWorkspace;
private string selectedWorkspaceName = string.Empty;
private string[] dataIssues = []; private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty; private string dataEditingPreviousName = string.Empty;
private bool isInlineEditOnGoing; private bool isInlineEditOnGoing;
@@ -138,6 +163,14 @@ public partial class ChatTemplateDialog : MSGComponentBase
// them, so the switch inside the selection starts on instead of at its usual default: // them, so the switch inside the selection starts on instead of at its usual default:
this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false }; this.templateDataSourceOptions = this.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false };
// The profile and the provider follow the same pattern, with a switch each:
this.preselectProfile = this.ProfileId is not null;
this.selectedProfileId = this.ProfileId ?? Profile.NO_PROFILE.Id;
this.preselectProvider = this.ProviderId is not null;
this.selectedProviderId = this.ProviderId ?? string.Empty;
this.preselectWorkspace = this.WorkspaceName is not null;
this.selectedWorkspaceName = this.WorkspaceName ?? string.Empty;
// When editing, we need to load the data: // When editing, we need to load the data:
if(this.IsEditing) if(this.IsEditing)
this.dataEditingPreviousName = this.DataName.ToLowerInvariant(); this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
@@ -183,6 +216,11 @@ public partial class ChatTemplateDialog : MSGComponentBase
ToolIds = this.preselectTools ? new HashSet<string>(this.selectedToolIds, StringComparer.Ordinal) : null, ToolIds = this.preselectTools ? new HashSet<string>(this.selectedToolIds, StringComparer.Ordinal) : null,
DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null, DataSourceOptions = this.preselectDataSources ? this.templateDataSourceOptions.CreateCopy() : null,
// A template which forbids profiles has no use for a profile, so none is kept for it:
ProfileId = this.AllowProfileUsage && this.preselectProfile ? this.selectedProfileId : null,
ProviderId = this.preselectProvider && !string.IsNullOrWhiteSpace(this.selectedProviderId) ? this.selectedProviderId : null,
WorkspaceName = this.preselectWorkspace && !string.IsNullOrWhiteSpace(this.selectedWorkspaceName) ? this.selectedWorkspaceName.Trim() : null,
EnterpriseConfigurationPluginId = Guid.Empty, EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false, IsEnterpriseConfiguration = false,
}; };
@@ -205,6 +243,57 @@ public partial class ChatTemplateDialog : MSGComponentBase
private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds; private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds;
/// <summary>
/// Whether the preselected profile is gone, so that chats started with this template keep their usual profile.
/// </summary>
private bool IsPreselectedProfileMissing => this.preselectProfile
&& this.selectedProfileId != Profile.NO_PROFILE.Id
&& this.SettingsManager.GetProfileById(this.selectedProfileId) == Profile.NO_PROFILE;
/// <summary>
/// Whether the preselected provider is gone or may not be used for chats, so that chats started
/// with this template keep their usual provider.
/// </summary>
/// <remarks>
/// The list to pick from holds only the providers which may be used for chats. A template from
/// another machine or from a configuration plugin may name another one, though, and the list
/// alone would then show nothing about it.
/// </remarks>
private bool IsPreselectedProviderUnusable => this.preselectProvider
&& !string.IsNullOrWhiteSpace(this.selectedProviderId)
&& this.SettingsManager.GetChatTemplateProvider(this.selectedProviderId) == AIStudio.Settings.Provider.NONE;
/// <summary>
/// Whether workspaces are switched off, so that a workspace of this template has no effect for now.
/// </summary>
private bool AreWorkspacesDisabled => this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES;
/// <summary>
/// Whether the workspace of this template can be changed right now.
/// </summary>
/// <remarks>
/// Not while workspaces are switched off: a choice made then would take effect for no chat,
/// and nothing in the chat would say so. A workspace the template already names stays as it
/// is, so it applies again once workspaces are back.
/// </remarks>
private bool IsWorkspaceChoiceLocked => this.IsReadOnly || this.AreWorkspacesDisabled;
private string? ValidateWorkspaceName(string workspaceName)
{
if (this.preselectWorkspace && string.IsNullOrWhiteSpace(workspaceName))
return T("Please enter the workspace for the chats of this template, or switch the preselection off.");
return null;
}
private string? ValidatePreselectedProvider(string providerId)
{
if (this.preselectProvider && string.IsNullOrWhiteSpace(providerId))
return T("Please choose the provider this chat template preselects, or switch the preselection off.");
return null;
}
private void RemoveMessage(ContentBlock item) private void RemoveMessage(ContentBlock item)
{ {
if (this.IsReadOnly) if (this.IsReadOnly)
@@ -47,31 +47,7 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Select a root directory for this data source.") @T("Select a root directory for this data source.")
</MudJustifiedText> </MudJustifiedText>
@if (!this.CanChangeSource)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
@T("The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed.")
</MudAlert>
}
@if (this.CanChangeSource)
{
<SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" EnableDragDrop="true" CatchAllDocuments="true" /> <SelectDirectory @bind-Directory="@this.dataPath" Label="@T("Selected base directory for this data source")" DirectoryDialogTitle="@T("Select the base directory")" Validation="@this.dataSourceValidation.ValidatePath" EnableDragDrop="true" CatchAllDocuments="true" />
}
else
{
<MudTextField
T="string"
Text="@this.dataPath"
Label="@T("Selected base directory for this data source")"
Class="mb-3"
ReadOnly="@true"
Validation="@this.dataSourceValidation.ValidatePath"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
}
<MudTextSwitch <MudTextSwitch
Label="@T("Include subfolders?")" Label="@T("Include subfolders?")"
@@ -23,16 +23,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
[Parameter] [Parameter]
public DataSourceLocalDirectory DataSource { get; set; } public DataSourceLocalDirectory DataSource { get; set; }
/// <summary>
/// Whether the folder this data source reads must stay as it is.
/// </summary>
/// <remarks>
/// Set once the index holds something for this data source. The embedding is not locked along
/// with it: it can be changed, and DataSourceReindexWarning asks what that costs.
/// </remarks>
[Parameter]
public bool LockSource { get; set; }
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@@ -215,8 +205,6 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
? T("Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.") ? T("Audio and video files which this data source selects are skipped, because your transcription provider does not meet the confidence level this data source requires.")
: null; : null;
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData(); private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null
@@ -244,9 +232,7 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
Type = DataSourceType.LOCAL_DIRECTORY, Type = DataSourceType.LOCAL_DIRECTORY,
EmbeddingId = this.dataEmbeddingId, EmbeddingId = this.dataEmbeddingId,
// Kept out of reach of the form while the source is locked, so a stale field cannot point an Path = this.dataPath,
// indexed data source somewhere else:
Path = this.CanChangeSource ? this.dataPath : this.DataSource.Path,
FilePatterns = this.NormalizeFilePatterns(), FilePatterns = this.NormalizeFilePatterns(),
// Kept as typed, since the line breaks group the entries for the eye: // Kept as typed, since the line breaks group the entries for the eye:
@@ -268,6 +254,14 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase
if (!this.dataIsValid) if (!this.dataIsValid)
return; return;
await this.StoreValidatedDataSourceAsync();
}
/// <summary>
/// Continues Save after form validation, asking before the draft is returned to its caller.
/// </summary>
internal async Task StoreValidatedDataSourceAsync()
{
var addedDataSource = this.CreateDataSource(); var addedDataSource = this.CreateDataSource();
// //
@@ -47,31 +47,7 @@
<MudJustifiedText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.") @T("Select a file for this data source. The content of this file will be processed for the data source. An audio or video file is transcribed by your transcription provider first.")
</MudJustifiedText> </MudJustifiedText>
@if (!this.CanChangeSource)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-3">
@T("The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed.")
</MudAlert>
}
@if (this.CanChangeSource)
{
<SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" EnableDragDrop="true" CatchAllDocuments="true" /> <SelectFile @bind-File="@this.dataFilePath" Label="@T("Selected file path for this data source")" FileDialogTitle="@T("Select the file")" Validation="@this.dataSourceValidation.ValidateFilePath" EnableDragDrop="true" CatchAllDocuments="true" />
}
else
{
<MudTextField
T="string"
Text="@this.dataFilePath"
Label="@T("Selected file path for this data source")"
Class="mb-3"
ReadOnly="@true"
Validation="@this.dataSourceValidation.ValidateFilePath"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
}
<MudJustifiedText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.") @T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.")
@@ -24,16 +24,6 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
[Parameter] [Parameter]
public DataSourceLocalFile DataSource { get; set; } public DataSourceLocalFile DataSource { get; set; }
/// <summary>
/// Whether the file this data source reads must stay as it is.
/// </summary>
/// <remarks>
/// Set once the index holds something for this data source. The embedding is not locked along
/// with it: it can be changed, and DataSourceReindexWarning asks what that costs.
/// </remarks>
[Parameter]
public bool LockSource { get; set; }
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@@ -206,8 +196,6 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
? T("This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.") ? T("This audio or video file is skipped, because your transcription provider does not meet the confidence level this data source requires.")
: null; : null;
private bool CanChangeSource => !this.IsEditing || !this.LockSource;
private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData(); private IEnumerable<ConfigurationSelectData<ConfidenceLevel>> ConfidenceLevels => ConfigurationSelectDataFactory.GetDataSourceConfidenceLevelsData();
private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null private string SelectedEmbeddingTokenizerText => this.SelectedEmbedding is null
@@ -235,9 +223,7 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
Type = DataSourceType.LOCAL_FILE, Type = DataSourceType.LOCAL_FILE,
EmbeddingId = this.dataEmbeddingId, EmbeddingId = this.dataEmbeddingId,
// Kept out of reach of the form while the source is locked, so a stale field cannot point an FilePath = this.dataFilePath,
// indexed data source somewhere else:
FilePath = this.CanChangeSource ? this.dataFilePath : this.DataSource.FilePath,
MaxChunkTokenLength = this.dataMaxChunkTokenLength, MaxChunkTokenLength = this.dataMaxChunkTokenLength,
ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength, ChunkOverlapTokenLength = this.dataChunkOverlapTokenLength,
ConfidenceLevel = this.dataConfidenceLevel, ConfidenceLevel = this.dataConfidenceLevel,
@@ -252,6 +238,14 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase
if (!this.dataIsValid) if (!this.dataIsValid)
return; return;
await this.StoreValidatedDataSourceAsync();
}
/// <summary>
/// Continues Save after form validation, asking before the draft is returned to its caller.
/// </summary>
internal async Task StoreValidatedDataSourceAsync()
{
var addedDataSource = this.CreateDataSource(); var addedDataSource = this.CreateDataSource();
// //
@@ -6,6 +6,12 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<MudDialog> <MudDialog>
<TitleContent>
<PreviewPrototype/>
<MudText Typo="Typo.h6">
@this.MudDialog.Title
</MudText>
</TitleContent>
<DialogContent> <DialogContent>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues" FieldChanged="@this.RevalidateAfterFieldChange"> <MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues" FieldChanged="@this.RevalidateAfterFieldChange">
@if (this.IsAddingNotAllowed) @if (this.IsAddingNotAllowed)
@@ -0,0 +1,26 @@
@using AIStudio.Settings
@inherits SettingsDialogBase
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6" Class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.AutoMode" Class="mr-2"/>
@T("Assistant: Assistant Builder defaults")
</MudText>
</TitleContent>
<DialogContent>
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="@T("Preselect Assistant Builder options?")" LabelOn="@T("Assistant Builder options are preselected")" LabelOff="@T("No Assistant Builder options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions = value)" OptionHelp="@T("When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Default output language")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesOptionalData()" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage = value)" OptionHelp="@T("The language in which the assistants you build answer.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedOutputLanguage, out var meta) && meta.IsLocked"/>
@if (this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOutputLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="@T("Default custom output language")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedOtherOutputLanguage = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedOtherOutputLanguage, out var meta) && meta.IsLocked"/>
}
<ConfigurationMinConfidenceSelection Disabled="@this.DefaultsDisabled" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.META_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.AssistantBuilder, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
</MudPaper>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,6 @@
namespace AIStudio.Dialogs.Settings;
public partial class SettingsDialogAssistantBuilder : SettingsDialogBase
{
private bool DefaultsDisabled() => !this.SettingsManager.ConfigurationData.AssistantBuilder.PreselectOptions;
}
@@ -21,7 +21,7 @@
<ConfigurationDirectory OptionDescription="@T("Default input folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default input folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory = value)" OptionHelp="@T("Leave empty when an input folder should be selected for every batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.InputDirectory, out var meta) && meta.IsLocked"/> <ConfigurationDirectory OptionDescription="@T("Default input folder")" Disabled="@this.DefaultsDisabled" DirectoryDialogTitle="@T("Select the default input folder")" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.InputDirectory = value)" OptionHelp="@T("Leave empty when an input folder should be selected for every batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.InputDirectory, out var meta) && meta.IsLocked"/>
<ConfigurationOption OptionDescription="@T("Include subfolders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Subfolders are included")" LabelOff="@T("Only the selected folder is processed")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeSubdirectories, out var meta) && meta.IsLocked"/> <ConfigurationOption OptionDescription="@T("Include subfolders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Subfolders are included")" LabelOff="@T("Only the selected folder is processed")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeSubdirectories = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeSubdirectories, out var meta) && meta.IsLocked"/>
<ConfigurationOption OptionDescription="@T("Include hidden files and folders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Hidden files and folders are included")" LabelOff="@T("Hidden files and folders are ignored")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders = value)" OptionHelp="@T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeHiddenFilesAndFolders, out var meta) && meta.IsLocked"/> <ConfigurationOption OptionDescription="@T("Include hidden files and folders by default?")" Disabled="@this.DefaultsDisabled" LabelOn="@T("Hidden files and folders are included")" LabelOff="@T("Hidden files and folders are ignored")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.IncludeHiddenFilesAndFolders = value)" OptionHelp="@T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.IncludeHiddenFilesAndFolders, out var meta) && meta.IsLocked"/>
<ConfigurationText OptionDescription="@T("Default file patterns")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAlt" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns = value)" ResetValue="@(() => DataBatchProcessing.DEFAULT_FILE_PATTERNS)" ResetButtonText="@T("Restore default patterns")" OptionHelp="@T("Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FilePatterns, out var meta) && meta.IsLocked"/> <ConfigurationText OptionDescription="@T("Default file patterns")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAlt" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FilePatterns = value)" ResetValue="@(() => DataBatchProcessing.DEFAULT_FILE_PATTERNS)" ResetButtonText="@T("Restore default patterns")" OptionHelp="@T("Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FilePatterns, out var meta) && meta.IsLocked"/>
<ConfigurationText OptionDescription="@T("Default excluded files and folders")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAltOff" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns = value)" NumLines="@FileExclusions.DEFAULT_LINE_COUNT" MaxLines="@FileExclusions.MAX_LINE_COUNT" ResetValue="@(() => FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@T("Restore default")" OptionHelp="@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/. Other entries name files, e.g., *.min.js.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ExcludePatterns, out var meta) && meta.IsLocked"/> <ConfigurationText OptionDescription="@T("Default excluded files and folders")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.FilterAltOff" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.ExcludePatterns = value)" NumLines="@FileExclusions.DEFAULT_LINE_COUNT" MaxLines="@FileExclusions.MAX_LINE_COUNT" ResetValue="@(() => FileExclusions.DEFAULT_PATTERNS)" ResetButtonText="@T("Restore default")" OptionHelp="@T("These files and folders are left out, even when a file pattern selects them. Separate entries with a semicolon or a line break. End an entry with a slash to exclude every folder of that name, e.g., node_modules/. Other entries name files, e.g., *.min.js.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.ExcludePatterns, out var meta) && meta.IsLocked"/>
<ConfigurationNumber T="int" OptionDescription="@T("Ignore files larger than")" Min="@FolderFileSelection.SMALLEST_MAX_FILE_SIZE_MEGABYTES" Unit="MB" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes = value)" OptionHelp="@T("Matching files above this size are left out of a batch run, e.g., images of virtual machines.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MaxFileSizeMegabytes, out var meta) && meta.IsLocked"/> <ConfigurationNumber T="int" OptionDescription="@T("Ignore files larger than")" Min="@FolderFileSelection.SMALLEST_MAX_FILE_SIZE_MEGABYTES" Unit="MB" Disabled="@this.DefaultsDisabled" Value="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes)" ValueUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MaxFileSizeMegabytes = value)" OptionHelp="@T("Matching files above this size are left out of a batch run, e.g., images of virtual machines.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MaxFileSizeMegabytes, out var meta) && meta.IsLocked"/>
@@ -114,6 +114,9 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
{ x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage }, { x => x.AllowProfileUsage, chatTemplate.AllowProfileUsage },
{ x => x.ToolIds, chatTemplate.ToolIds }, { x => x.ToolIds, chatTemplate.ToolIds },
{ x => x.DataSourceOptions, chatTemplate.DataSourceOptions }, { x => x.DataSourceOptions, chatTemplate.DataSourceOptions },
{ x => x.ProfileId, chatTemplate.ProfileId },
{ x => x.ProviderId, chatTemplate.ProviderId },
{ x => x.WorkspaceName, chatTemplate.WorkspaceName },
}; };
private async Task DeleteChatTemplate(ChatTemplate chatTemplate) private async Task DeleteChatTemplate(ChatTemplate chatTemplate)
@@ -19,7 +19,7 @@
<AssistantBlock TSettings="SettingsDialogRewrite" Component="Components.REWRITE_ASSISTANT" Name="@T("Rewrite & Improve")" Description="@T("Rewrite and improve a given text for a chosen style.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/> <AssistantBlock TSettings="SettingsDialogRewrite" Component="Components.REWRITE_ASSISTANT" Name="@T("Rewrite & Improve")" Description="@T("Rewrite and improve a given text for a chosen style.")" Icon="@Icons.Material.Filled.Edit" Link="@Routes.ASSISTANT_REWRITE"/>
<AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/> <AssistantBlock TSettings="SettingsDialogPromptOptimizer" Component="Components.PROMPT_OPTIMIZER_ASSISTANT" Name="@T("Prompt Optimizer")" Description="@T("Optimize your prompt using a structured guideline.")" Icon="@Icons.Material.Filled.AutoFixHigh" Link="@Routes.ASSISTANT_PROMPT_OPTIMIZER"/>
<AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/> <AssistantBlock TSettings="SettingsDialogSynonyms" Component="Components.SYNONYMS_ASSISTANT" Name="@T("Synonyms")" Description="@T("Find synonyms for a given word or phrase.")" Icon="@Icons.Material.Filled.Spellcheck" Link="@Routes.ASSISTANT_SYNONYMS"/>
<AssistantBlock TSettings="NoSettingsPanel" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/> <AssistantBlock TSettings="SettingsDialogAssistantBuilder" Component="Components.META_ASSISTANT" RequiredPreviewFeature="PreviewFeatures.PRE_META_ASSISTANT_V1" Name="@T("Assistant Builder")" Description="@T("Generate your own assistants.")" Icon="@Icons.Material.Filled.AutoMode" Link="@Routes.ASSISTANT_META_ASSISTANT"/>
</AssistantCategoryBlock> </AssistantCategoryBlock>
@if (this.AssistantPlugins.Count > 0) @if (this.AssistantPlugins.Count > 0)
@@ -296,7 +296,7 @@
@* @*
What the user has to decide comes with the buttons to decide it. The What the user has to decide comes with the buttons to decide it. The
sentence above says what is at stake; reading it and then having to find sentence above says what is at stake; reading it and then having to find
the settings of the mailbox is where people give up. the settings of the mailbox or the data source is where people give up.
*@ *@
@if (status.Attention is DataSourceAttention.AUTH_FAILED) @if (status.Attention is DataSourceAttention.AUTH_FAILED)
{ {
@@ -320,6 +320,14 @@
</MudButton> </MudButton>
</MudStack> </MudStack>
} }
else if (status.Attention is DataSourceAttention.LOCATION_UNREACHABLE && this.CanEditDataSource(status))
{
<MudStack Row="@true" Wrap="Wrap.Wrap" Spacing="2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Edit" OnClick="@(async () => await this.EditDataSource(status))">
@T("Edit this data source")
</MudButton>
</MudStack>
}
</MudStack> </MudStack>
</ChildContent> </ChildContent>
</MudExpansionPanel> </MudExpansionPanel>
@@ -509,6 +509,25 @@ public partial class Embeddings : MSGComponentBase
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
} }
/// <remarks>
/// Not offered for a data source the organization manages: its settings cannot be edited.
/// </remarks>
private bool CanEditDataSource(DataSourceEmbeddingStatus status) => DataSourceEditing.CanEdit(this.SettingsManager, status.DataSourceId);
/// <summary>
/// Opens the settings of a data source, e.g. to select the new location of a moved folder.
/// </summary>
/// <remarks>
/// Saving them indexes the data source right away, and that run reconnects what is already
/// stored to the new location. The page reloads through CONFIGURATION_CHANGED, which it
/// receives like everybody else.
/// </remarks>
private async Task EditDataSource(DataSourceEmbeddingStatus status)
{
if (await DataSourceEditing.EditAsync(this.DialogService, this.SettingsManager, this.DataSourceEmbeddingService, status.DataSourceId))
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
}
/// <summary> /// <summary>
/// Agrees to the removal a mailbox held back, for the very number the user read. /// Agrees to the removal a mailbox held back, for the very number the user read.
/// </summary> /// </summary>
@@ -153,6 +153,8 @@ ASSISTANT = {
- A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt. - A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt.
- When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat. - When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat.
- A chat template may preselect tools and data sources as well. When it does, it decides them alone: the launcher `ToolIds` and `DataSourceIds` are dropped, and AI Studio writes a warning into the log naming both sides. The rule is the same for tools and for data sources, so there is only one to remember. - A chat template may preselect tools and data sources as well. When it does, it decides them alone: the launcher `ToolIds` and `DataSourceIds` are dropped, and AI Studio writes a warning into the log naming both sides. The rule is the same for tools and for data sources, so there is only one to remember.
- A chat template may also preselect a provider and a profile, and the same rule applies: they win over the launcher `ProviderId` and `ProfileId`, with a warning in the log. A provider of the template which is gone or does not meet the confidence level required for chats, and a profile of the template which is gone, are left out instead of stopping the launch; the launcher then decides as if the template named none.
- A chat template may name a workspace for its chats as well. Here the launcher wins when it names a workspace of its own, because that choice was made on purpose. A launcher that opens a disappearing chat opens it in the workspace of its template instead, which is created when it does not exist yet; if that fails, the chat stays a disappearing chat.
- The reason the template wins as a whole rather than field by field is a difference in what the two can express. A launcher can only ever say "these sources, picked by hand", while a chat template carries the whole data source options and can also say "let an agent pick the sources for each message". A mix of both would be something neither of them asked for. - The reason the template wins as a whole rather than field by field is a difference in what the two can express. A launcher can only ever say "these sources, picked by hand", while a chat template carries the whole data source options and can also say "let an agent pick the sources for each message". A mix of both would be something neither of them asked for.
- The data sources of a chat template are checked exactly like the ones of a launcher: a source which no longer exists, or which is not permitted for the effective provider, stops the launch before a workspace or chat is created. The message then names the chat template as the cause, not the launcher. A template which leaves the choice to an agent names no source and is therefore not checked here; that decision is made per message in the chat. - The data sources of a chat template are checked exactly like the ones of a launcher: a source which no longer exists, or which is not permitted for the effective provider, stops the launch before a workspace or chat is created. The message then names the chat template as the cause, not the launcher. A template which leaves the choice to an agent names no source and is therefore not checked here; that decision is made per message in the chat.
- The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there. - The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there.
@@ -650,7 +650,7 @@ CONFIG["SETTINGS"] = {}
-- --
-- Configure the default file patterns and whether subfolders as well as hidden files and folders are included. -- Configure the default file patterns and whether subfolders as well as hidden files and folders are included.
-- Separate multiple patterns with semicolons. -- Separate multiple patterns with semicolons.
-- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm" -- CONFIG["SETTINGS"]["DataBatchProcessing.FilePatterns"] = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.jpg;*.jpeg;*.png;*.webp;*.gif;*.bmp;*.tif;*.tiff;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm"
-- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeSubdirectories"] = false -- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeSubdirectories"] = false
-- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeHiddenFilesAndFolders"] = false -- CONFIG["SETTINGS"]["DataBatchProcessing.IncludeHiddenFilesAndFolders"] = false
-- --
@@ -818,6 +818,37 @@ CONFIG["SETTINGS"] = {}
-- documents. Providers below this level cannot be selected in the assistant. -- documents. Providers below this level cannot be selected in the assistant.
-- CONFIG["SETTINGS"]["DataVisualBriefing.MinimumProviderConfidence"] = "NONE" -- CONFIG["SETTINGS"]["DataVisualBriefing.MinimumProviderConfidence"] = "NONE"
-- Configure defaults for the Assistant Builder.
-- Preselection must be enabled for the remaining Assistant Builder settings to take effect.
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectOptions"] = true
--
-- Configure the preselected provider for building assistants.
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- Without a preselected provider, the Assistant Builder uses the app-wide default provider.
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the language in which the generated assistants answer.
-- Allowed values are: AS_IS, EN_US, EN_GB, ZH_CN, HI_IN, ES_ES, FR_FR, DE_DE, DE_CH, DE_AT,
-- JA_JP, RU_RU, OTHER
-- AS_IS leaves the output language of the generated assistants open.
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOutputLanguage"] = "AS_IS"
--
-- Configure a free-form language, used only when PreselectedOutputLanguage is "OTHER".
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOtherOutputLanguage"] = ""
--
-- Configure the minimum provider confidence required to build assistants.
-- Allowed values are: NONE, UNTRUSTED, UNKNOWN, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
-- Providers below this level cannot be selected in the Assistant Builder. This minimum applies
-- only while preselection is enabled; the app-wide minimum confidence applies in any case.
-- CONFIG["SETTINGS"]["DataAssistantBuilder.MinimumProviderConfidence"] = "NONE"
--
-- Allow users to change individual managed Assistant Builder defaults locally.
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectOptions.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedProvider.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOutputLanguage.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataAssistantBuilder.PreselectedOtherOutputLanguage.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataAssistantBuilder.MinimumProviderConfidence.AllowUserOverride"] = true
-- Configure enterprise approvals for assistant plugins. -- Configure enterprise approvals for assistant plugins.
-- Each approval is matched only by the current SHA-256 hash over all Lua files -- Each approval is matched only by the current SHA-256 hash over all Lua files
-- in the assistant plugin folder, in canonical sorted order. -- in the assistant plugin folder, in canonical sorted order.
@@ -1363,6 +1394,42 @@ CONFIG["CHAT_TEMPLATES"] = {}
-- }, -- },
-- } -- }
-- An example chat template which preselects a provider, a profile, and a workspace:
-- All three are optional and independent of each other, and leaving one out keeps what a
-- chat starts with anyway. They are a preselection, not a limit: users change the provider
-- and the profile in the chat as usual, and move the chat to another workspace. The IDs may
-- name a provider or profile of this configuration, which keeps its ID on every computer, or
-- one of another configuration of your organization.
-- CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
-- ["Id"] = "00000000-0000-0000-0000-000000000003",
-- ["Name"] = "Mail Assistant",
-- ["SystemPrompt"] = "You help <Company Name>'s employees read, sort, and answer their mails.",
-- ["AllowProfileUsage"] = true,
--
-- -- Optional: the provider, and with it the model, a chat with this template starts with.
-- -- Must be an ID from CONFIG["LLM_PROVIDERS"] or of a provider of another configuration.
-- -- The provider has to meet the confidence level required for chats. A provider which
-- -- does not, or an ID which resolves to nothing, is ignored, and the chat starts with
-- -- the provider it would start with anyway.
-- ["ProviderId"] = "11111111-1111-1111-1111-111111111111",
--
-- -- Optional: the profile a chat with this template starts with.
-- -- Must be an ID from CONFIG["PROFILES"] or of a profile of another configuration.
-- -- "00000000-0000-0000-0000-000000000000" starts the chat without a profile, whatever
-- -- the user chose as their default. An ID which resolves to nothing is ignored, and the
-- -- chat starts with the default profile of the user. The whole field is ignored when
-- -- AllowProfileUsage is false.
-- ["ProfileId"] = "22222222-2222-2222-2222-222222222222",
--
-- -- Optional: the workspace, by its name, that keeps the chats started with this template.
-- -- The workspace is created with the first message of such a chat, when it does not exist
-- -- yet. It only applies where a chat would otherwise be a disappearing chat: a chat users
-- -- start in a workspace on purpose, for example, from the workspace list, stays there, and
-- -- so does every chat while workspaces are switched off. An assistant chat launcher which
-- -- names a workspace keeps its own.
-- ["WorkspaceName"] = "Mails",
-- }
-- Introduction texts shown as expansion panels on the welcome page: -- Introduction texts shown as expansion panels on the welcome page:
CONFIG["INTRODUCTIONS"] = {} CONFIG["INTRODUCTIONS"] = {}
@@ -426,6 +426,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The selected folder does not exist. -- The selected folder does not exist.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "Der ausgewählte Ordner existiert nicht." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "Der ausgewählte Ordner existiert nicht."
-- The image was no longer available while the model was asked about it.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1805791865"] = "Das Bild war nicht mehr verfügbar, während das Modell dazu befragt wurde."
-- Minimum pause between files -- Minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Mindestpause zwischen Dateien" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Mindestpause zwischen Dateien"
@@ -531,9 +534,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Was not able to write the result file: {0} -- Was not able to write the result file: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Die Ergebnisdatei konnte nicht geschrieben werden: {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Die Ergebnisdatei konnte nicht geschrieben werden: {0}"
-- Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3011193639"] = "Bilder im JPG-, PNG-, WebP-, GIF-, BMP- oder TIFF-Format werden als Bilder an das Modell übergeben, daher benötigen Sie ein Modell, das Bilder versteht. Falls das ausgewählte Modell keine Bilder lesen kann, entfernen Sie die Dateimuster für Bilder, z. B. *.jpg oder *.png, bevor Sie starten."
-- The batch run finished, but one file could not be processed. See the progress table and log for details. -- The batch run finished, but one file could not be processed. See the progress table and log for details.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "Die Stapelverarbeitung ist abgeschlossen, aber eine Datei konnte nicht verarbeitet werden. Weitere Informationen finden Sie in der Fortschrittstabelle und im Protokoll." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "Die Stapelverarbeitung ist abgeschlossen, aber eine Datei konnte nicht verarbeitet werden. Weitere Informationen finden Sie in der Fortschrittstabelle und im Protokoll."
-- The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T324865195"] = "Das ausgewählte Modell kann keine Bilder lesen, daher schlägt die Verarbeitung der {0} Bilder unter den ausgewählten Dateien fehl. Alle anderen Dateien werden wie gewohnt verarbeitet."
-- Maximum pause between files -- Maximum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximale Pause zwischen Dateien" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximale Pause zwischen Dateien"
@@ -552,6 +561,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Show Expert Settings -- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Experten-Einstellungen anzeigen" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Experten-Einstellungen anzeigen"
-- All selected files are images, but the selected model cannot read images. Select a model with image support.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3365102967"] = "Alle ausgewählten Dateien sind Bilder, aber das ausgewählte Modell kann keine Bilder lesen. Wählen Sie ein Modell mit Bildunterstützung aus."
-- Header of the result column (optional) -- Header of the result column (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Überschrift der Ergebnisspalte (optional)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Überschrift der Ergebnisspalte (optional)"
@@ -564,6 +576,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'. -- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "Der Dateiname der CSV-Ergebnistabelle. Die Endung .csv wird ergänzt, falls sie fehlt. Wenn Sie das Feld leer lassen, wird 'batch-results.csv' verwendet." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "Der Dateiname der CSV-Ergebnistabelle. Die Endung .csv wird ergänzt, falls sie fehlt. Wenn Sie das Feld leer lassen, wird 'batch-results.csv' verwendet."
-- The selected model cannot read images. Select a model with image support to process this image.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T346986514"] = "Das ausgewählte Modell kann keine Bilder lesen. Wählen Sie ein Modell mit Bildunterstützung aus, um dieses Bild zu verarbeiten."
-- Document analysis policy -- Document analysis policy
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Regelwerk für die Dokumentenanalyse" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Regelwerk für die Dokumentenanalyse"
@@ -678,6 +693,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here. -- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "Die KI kann diese Werkzeuge bei der Arbeit an jedem Dokument verwenden. Jedes Werkzeug muss die Vertrauensanforderungen des ausgewählten Anbieters erfüllen. Daher kann ein Werkzeug trotz Auswahl hier weiterhin nicht verfügbar sein." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "Die KI kann diese Werkzeuge bei der Arbeit an jedem Dokument verwenden. Jedes Werkzeug muss die Vertrauensanforderungen des ausgewählten Anbieters erfüllen. Daher kann ein Werkzeug trotz Auswahl hier weiterhin nicht verfügbar sein."
-- The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T9738678"] = "Das ausgewählte Modell kann keine Bilder lesen, daher schlägt die Verarbeitung des Bildes unter den ausgewählten Dateien fehl. Alle anderen Dateien werden wie gewohnt verarbeitet."
-- Comma (,) -- Comma (,)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Komma (,)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Komma (,)"
@@ -4122,9 +4140,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Si
-- Automatic local data source refresh -- Automatic local data source refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatische Aktualisierung lokaler Datenquellen" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatische Aktualisierung lokaler Datenquellen"
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1215599168"] = "Datenquelle bearbeiten: Lokaler Ordner"
-- Refresh -- Refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Aktualisieren" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Aktualisieren"
@@ -4152,12 +4167,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "M
-- Local Directory Data Source Information -- Local Directory Data Source Information
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Informationen zur lokalen Ordner-Datenquelle" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Informationen zur lokalen Ordner-Datenquelle"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T221059217"] = "ERI v1 Datenquelle bearbeiten"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2453292893"] = "Datenquelle bearbeiten: Lokale Datei"
-- Local data sources refresh when files change, mailboxes every 16 minutes. -- Local data sources refresh when files change, mailboxes every 16 minutes.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Lokale Datenquellen werden aktualisiert, wenn sich Dateien ändern, Postfächer alle 16 Minuten." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Lokale Datenquellen werden aktualisiert, wenn sich Dateien ändern, Postfächer alle 16 Minuten."
@@ -4392,9 +4401,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = "
-- Workspace name (Optional) -- Workspace name (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Name des Arbeitsbereichs (optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Name des Arbeitsbereichs (optional)"
-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "Die ausgewählte Chat-Vorlage wählt einen eigenen Anbieter vorab aus; dieser hat Vorrang vor einer hier getroffenen Auswahl, solange er für Chats verwendet werden kann."
-- Use no profile -- Use no profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Kein Profil verwenden" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Kein Profil verwenden"
-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "Die ausgewählte Chat-Vorlage legt ihre Chats im Arbeitsbereich „{0}“ ab, daher öffnet die Kachel ihren Chat dort. Ein hier eingegebener Arbeitsbereich hat Vorrang vor dem der Vorlage."
-- Existing workspace (Optional) -- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Vorhandener Arbeitsbereich (optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Vorhandener Arbeitsbereich (optional)"
@@ -4416,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = "
-- Data sources (Optional) -- Data sources (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Datenquellen (optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Datenquellen (optional)"
-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "Die ausgewählte Chat-Vorlage wählt ein eigenes Profil vorab aus; dieses hat Vorrang vor einer hier getroffenen Auswahl, solange das Profil existiert."
-- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Ohne Arbeitsbereich öffnet die Kachel einen selbstlöschenden Chat: Er gehört keinem Arbeitsbereich an und wird gemäß den Wartungseinstellungen für Ihre Arbeitsbereiche gelöscht." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Ohne Arbeitsbereich öffnet die Kachel einen selbstlöschenden Chat: Er gehört keinem Arbeitsbereich an und wird gemäß den Wartungseinstellungen für Ihre Arbeitsbereiche gelöscht."
@@ -6180,14 +6198,23 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "Der Nam
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. -- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "Das Gleiche gilt für Ihre Daten. Eine Chat-Vorlage kann eigene Datenquellen-Optionen mitbringen – dazu gehört auch, die Auswahl der Quellen der KI zu überlassen. Ohne solche Optionen starten diese Chats mit den Datenquellen-Optionen aus Ihren Chat-Optionen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "Das Gleiche gilt für Ihre Daten. Eine Chat-Vorlage kann eigene Datenquellen-Optionen mitbringen – dazu gehört auch, die Auswahl der Quellen der KI zu überlassen. Ohne solche Optionen starten diese Chats mit den Datenquellen-Optionen aus Ihren Chat-Optionen."
-- Preselected provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Vorausgewählter Anbieter"
-- Please enter a name for the chat template. -- Please enter a name for the chat template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Bitte geben Sie einen Namen für die Chat-Vorlage ein." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Bitte geben Sie einen Namen für die Chat-Vorlage ein."
-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "Eine Chat-Vorlage kann festlegen, mit welchem Anbieter – und damit mit welchem Modell – ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit dem Anbieter, mit dem sie ohnehin starten würden. Der Anbieter lässt sich im Chat weiterhin ändern und muss das für Chats erforderliche Vertrauensniveau erfüllen."
-- Yes, this template decides which data a chat starts with -- Yes, this template decides which data a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Ja, diese Vorlage legt fest, mit welchen Daten ein Chat startet" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Ja, diese Vorlage legt fest, mit welchen Daten ein Chat startet"
-- Preselected profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Vorausgewähltes Profil"
-- Enter the full path of a file on this computer, or remove the attachment. -- Enter the full path of a file on this computer, or remove the attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Geben Sie den vollständigen Pfad zu einer Datei auf diesem Computer ein oder entfernen Sie den Anhang." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Geben Sie den vollständigen Pfad zu einer Datei auf diesem Rechner ein oder entfernen Sie den Anhang."
-- Load predefined user input from file -- Load predefined user input from file
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefinierte Benutzereingabe aus Datei laden" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefinierte Benutzereingabe aus Datei laden"
@@ -6195,12 +6222,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Vordefi
-- Update -- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Aktualisieren" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Aktualisieren"
-- Yes, this template decides which profile a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Ja, diese Vorlage legt fest, mit welchem Profil ein Chat startet"
-- The chat template name must not exceed 40 characters. -- The chat template name must not exceed 40 characters.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "Der Name der Chat-Vorlage darf maximal 40 Zeichen lang sein." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "Der Name der Chat-Vorlage darf maximal 40 Zeichen lang sein."
-- Profile Usage -- Profile Usage
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profilnutzung" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profilnutzung"
-- Use no profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Kein Profil verwenden"
-- Yes, chats with this template go into a workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Ja, Chats mit dieser Vorlage werden in einem Arbeitsbereich abgelegt"
-- Relink attachment: {0} -- Relink attachment: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Anhang erneut verknüpfen: {0}" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Anhang erneut verknüpfen: {0}"
@@ -6210,6 +6246,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Fügen
-- File Attachments -- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateianhänge" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "Dateianhänge"
-- No, chats go where they would go anyway
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "Nein, Chats werden dort abgelegt, wo sie ohnehin landen würden"
-- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Vorhandener Arbeitsbereich (optional)"
-- Role -- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Rolle"
@@ -6222,6 +6264,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Werkzeu
-- What predefined user input do you want to use? -- What predefined user input do you want to use?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "Welche vordefinierte Benutzereingabe möchten Sie verwenden?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "Welche vordefinierte Benutzereingabe möchten Sie verwenden?"
-- Please enter the workspace for the chats of this template, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Bitte geben Sie den Arbeitsbereich für die Chats dieser Vorlage ein oder deaktivieren Sie die Vorauswahl."
-- Does this chat template preselect a provider?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Wählt diese Chat-Vorlage einen Anbieter vorab aus?"
-- Tell the AI your system prompt. -- Tell the AI your system prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Teilen Sie der KI einen System-Prompt mit." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Teilen Sie der KI einen System-Prompt mit."
@@ -6231,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "Nein, d
-- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "Sie können eine Nachricht vordefinieren, die als Benutzereingabe verwendet wird, wenn Sie diese Chat-Vorlage verwenden. Diese Nachricht könnte beispielsweise eine Vorlage für eine strukturierte Nachricht sein, für die diese Chat-Vorlage entwickelt wurde." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "Sie können eine Nachricht vordefinieren, die als Benutzereingabe verwendet wird, wenn Sie diese Chat-Vorlage verwenden. Diese Nachricht könnte beispielsweise eine Vorlage für eine strukturierte Nachricht sein, für die diese Chat-Vorlage entwickelt wurde."
-- Does this chat template preselect a profile?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Wählt diese Chat-Vorlage ein Profil vorab aus?"
-- Predefined User Input -- Predefined User Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Vordefinierte Benutzereingabe" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Vordefinierte Benutzereingabe"
@@ -6246,8 +6297,14 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "Die Nac
-- Chat Template Name -- Chat Template Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Name der Chat-Vorlage" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Name der Chat-Vorlage"
-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "In Ihren Arbeitsbereichseinstellungen sind Arbeitsbereiche deaktiviert. Solange sie deaktiviert sind, gehören Chats mit dieser Vorlage zu keinem Arbeitsbereich und werden auch nicht in einen Arbeitsbereich verschoben, wenn Sie Arbeitsbereiche wieder aktivieren. Sie können diese Auswahl ändern, sobald Arbeitsbereiche aktiviert sind."
-- Please relink the attachment '{0}' to a file on this computer or remove it before you save. -- Please relink the attachment '{0}' to a file on this computer or remove it before you save.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Bitte verknüpfen Sie den Anhang „{0}“ vor dem Speichern erneut mit einer Datei auf diesem Computer oder entfernen Sie ihn." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Bitte verknüpfen Sie den Anhang „{0}“ vor dem Speichern erneut mit einer Datei auf diesem Rechner oder entfernen Sie ihn."
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Name des Arbeitsbereichs"
-- Commit Changes -- Commit Changes
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Änderungen übernehmen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Änderungen übernehmen"
@@ -6264,21 +6321,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "Sie kö
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Sie unsicher, welchen System-Prompt Sie verwenden sollen? Sie können mit dem Standard-System-Prompt beginnen, den AI Studio für alle Chats verwendet." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Sind Sie unsicher, welchen System-Prompt Sie verwenden sollen? Sie können mit dem Standard-System-Prompt beginnen, den AI Studio für alle Chats verwendet."
-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "Eine Chat-Vorlage kann auch festlegen, mit welchem Profil ein Chat startet, oder dass er ohne Profil startet. Ohne eine solche Festlegung starten diese Chats mit dem Profil, mit dem sie ohnehin starten würden. Das Profil lässt sich im Chat weiterhin ändern."
-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "Das Profil, das diese Chat-Vorlage vorab auswählt, ist auf diesem Rechner nicht vorhanden. Mit ihr gestartete Chats behalten ihr übliches Profil."
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Die gleichzeitige Verwendung einiger Chat-Vorlagen mit Profilen kann zu Problemen führen. Deshalb könnten Sie hier die Nutzung von Profilen untersagen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Die gleichzeitige Verwendung einiger Chat-Vorlagen mit Profilen kann zu Problemen führen. Deshalb könnten Sie hier die Nutzung von Profilen untersagen."
-- Choose an existing workspace or enter a name that should be created when a chat with this template starts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Wählen Sie einen vorhandenen Arbeitsbereich aus oder geben Sie einen Namen ein, der erstellt werden soll, wenn ein Chat mit dieser Vorlage startet."
-- No, chats keep the data source options from your chat options -- No, chats keep the data source options from your chat options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "Nein, Chats behalten die Datenquellen-Optionen aus Ihren Chat-Optionen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "Nein, Chats behalten die Datenquellen-Optionen aus Ihren Chat-Optionen"
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. -- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "Eine Chat-Vorlage kann festlegen, mit welchen Werkzeugen ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit den Werkzeugen, die Sie in den Chat-Optionen als Standard ausgewählt haben. Legen Sie es fest und wählen dann nichts aus, ist das eine andere Aussage: Solche Chats starten ohne jedes Werkzeug, ganz gleich, was Ihr Standard vorsieht." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "Eine Chat-Vorlage kann festlegen, mit welchen Werkzeugen ein Chat startet. Ohne eine solche Festlegung starten diese Chats mit den Werkzeugen, die Sie in den Chat-Optionen als Standard ausgewählt haben. Legen Sie es fest und wählen dann nichts aus, ist das eine andere Aussage: Solche Chats starten ohne jedes Werkzeug, ganz gleich, was Ihr Standard vorsieht."
-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "Eine Chat-Vorlage kann ihre Chats in einem eigenen Arbeitsbereich ablegen, der erstellt wird, falls er noch nicht existiert. Das gilt immer dann, wenn ein Chat sonst ein selbstlöschender Chat wäre, zum Beispiel wenn Sie einen neuen Chat außerhalb der Arbeitsbereiche starten oder diese Vorlage in einem neuen Chat auswählen. Ein Chat, den Sie gezielt in einem Arbeitsbereich starten, zum Beispiel über die Liste der Arbeitsbereiche, bleibt in diesem Arbeitsbereich."
-- Add a message -- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Nachricht hinzufügen"
-- Yes, this template decides which provider a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Ja, diese Vorlage legt fest, mit welchem Anbieter ein Chat startet"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Schließen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Schließen"
-- No, chats keep their usual profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "Nein, Chats behalten ihr übliches Profil"
-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "Der Anbieter, den diese Chat-Vorlage vorab auswählt, ist auf diesem Rechner nicht vorhanden oder erfüllt das für Chats erforderliche Vertrauensniveau nicht. Mit ihr gestartete Chats behalten ihren üblichen Anbieter."
-- Unsupported content type -- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Nicht unterstützter Inhaltstyp" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Nicht unterstützter Inhaltstyp"
@@ -6315,12 +6393,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Verwend
-- Tell the AI your predefined user input. -- Tell the AI your predefined user input.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Teilen Sie der KI Ihre vordefinierte Benutzereingabe mit." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Teilen Sie der KI Ihre vordefinierte Benutzereingabe mit."
-- Does this chat template keep its chats in a workspace?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Legt diese Chat-Vorlage ihre Chats in einem Arbeitsbereich ab?"
-- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Einige Anhänge dieser Chat-Vorlage sind auf diesem Computer nicht vorhanden. Bitte verknüpfen Sie sie erneut oder entfernen Sie sie, bevor Sie die Vorlage hinzufügen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Einige Anhänge dieser Chat-Vorlage sind auf diesem Rechner nicht vorhanden. Bitte verknüpfen Sie sie erneut oder entfernen Sie sie, bevor Sie die Vorlage hinzufügen."
-- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Erstellen Sie Ihre eigene Chat-Vorlage, um das Verhalten des LLMs für bestimmte Aufgaben oder Bereiche anzupassen. Definieren Sie einen individuellen System-Prompt und geben Sie eine Beispiel-Konversation vor, um eine KI-Erfahrung zu gestalten, die genau auf Ihre Anforderungen zugeschnitten ist." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Erstellen Sie Ihre eigene Chat-Vorlage, um das Verhalten des LLMs für bestimmte Aufgaben oder Bereiche anzupassen. Definieren Sie einen individuellen System-Prompt und geben Sie eine Beispiel-Konversation vor, um eine KI-Erfahrung zu gestalten, die genau auf Ihre Anforderungen zugeschnitten ist."
-- Please choose the provider this chat template preselects, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Bitte wählen Sie den Anbieter aus, den diese Chat-Vorlage vorab auswählt, oder deaktivieren Sie die Vorauswahl."
-- Enter a message -- Enter a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Nachricht eingeben" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Nachricht eingeben"
@@ -6333,15 +6417,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System-P
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. -- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "Ein mit dieser Vorlage gestarteter Chat beginnt mit diesen Datenquellen und Optionen. Alles davon lässt sich im Chat selbst weiterhin ändern." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "Ein mit dieser Vorlage gestarteter Chat beginnt mit diesen Datenquellen und Optionen. Alles davon lässt sich im Chat selbst weiterhin ändern."
-- Workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Arbeitsbereich"
-- Remove this attachment -- Remove this attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Diesen Anhang entfernen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Diesen Anhang entfernen"
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. -- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "Die Auswahl lässt sich im Chat weiterhin ändern, und jedes Werkzeug muss die Vertrauensanforderungen des verwendeten Anbieters erfüllen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "Die Auswahl lässt sich im Chat weiterhin ändern, und jedes Werkzeug muss die Vertrauensanforderungen des verwendeten Anbieters erfüllen."
-- No, chats keep their usual provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "Nein, Chats behalten ihren üblichen Anbieter"
-- Allow the use of profiles together with this chat template? -- Allow the use of profiles together with this chat template?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Erlauben Sie die Verwendung von Profilen zusammen mit dieser Chat-Vorlage?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Erlauben Sie die Verwendung von Profilen zusammen mit dieser Chat-Vorlage?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Anbieter"
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Abbrechen"
@@ -6807,9 +6900,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2460883298"
-- Add -- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Hinzufügen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Hinzufügen"
-- The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2686150329"] = "Die Dokumente dieser Datenquelle sind bereits vorbereitet, daher kann ihr Ordner nicht geändert werden. Ein anderer Ordner enthält andere Dokumente und ist damit eine andere Datenquelle: Bitte fügen Sie dafür eine neue hinzu. Den Einbettungsanbieter darunter können Sie ändern."
-- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud. -- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "Die von Ihnen ausgewählte Einbettung läuft lokal oder innerhalb Ihrer Organisation. Ihre Daten werden nicht in die Cloud übertragen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "Die von Ihnen ausgewählte Einbettung läuft lokal oder innerhalb Ihrer Organisation. Ihre Daten werden nicht in die Cloud übertragen."
@@ -7068,9 +7158,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T787118522"] = "D
-- Data Source Name -- Data Source Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Name der Datenquelle" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Name der Datenquelle"
-- The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T890383764"] = "Die Dokumente dieser Datenquelle sind bereits vorbereitet, daher kann ihre Datei nicht geändert werden. Eine andere Datei enthält andere Inhalte und ist damit eine andere Datenquelle: Bitte fügen Sie dafür eine neue hinzu. Den Einbettungsanbieter darunter können Sie ändern."
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Abbrechen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Abbrechen"
@@ -8580,6 +8667,33 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T6790
-- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model. -- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "Wenn diese Option aktiviert ist, können Sie Voreinstellungen vornehmen. Das kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "Wenn diese Option aktiviert ist, können Sie Voreinstellungen vornehmen. Das kann nützlich sein, wenn Sie eine bestimmte Sprache oder ein bestimmtes LLM-Modell bevorzugen."
-- When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T1496952909"] = "Wenn diese Option aktiviert ist, startet der Assistenten-Builder mit den unten festgelegten Standardwerten. Wenn sie deaktiviert ist, verwendet er den appweiten Standardanbieter und wählt keine Ausgabesprache vorab aus."
-- Preselect Assistant Builder options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T2127606148"] = "Optionen für den Assistenten-Builder vorauswählen?"
-- Default custom output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3360584376"] = "Standardmäßige benutzerdefinierte Ausgabesprache"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3448155331"] = "Schließen"
-- Default output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3652922567"] = "Standard-Ausgabesprache"
-- Assistant: Assistant Builder defaults
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3668274570"] = "Assistent: Standardwerte für den Assistenten-Builder"
-- Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3962364100"] = "Optionen für den Assistenten-Builder sind vorausgewählt"
-- No Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T4104323209"] = "Für den Assistenten-Builder sind keine Optionen vorausgewählt"
-- The language in which the assistants you build answer.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T42126731"] = "Die Sprache, in der die von Ihnen erstellten Assistenten antworten."
-- Default minimum pause between files -- Default minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Standardmäßige Mindestpause zwischen Dateien" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Standardmäßige Mindestpause zwischen Dateien"
@@ -8625,6 +8739,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T22
-- Hidden files and folders are included -- Hidden files and folders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Versteckte Dateien und Ordner werden einbezogen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Versteckte Dateien und Ordner werden einbezogen"
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T232588799"] = "Trennen Sie mehrere Dateimuster durch ein Semikolon, z. B. *.pdf;*.docx. Die Standardmuster umfassen alle unterstützten Audio- und Videoformate sowie Bilder im JPG-, PNG-, WebP-, GIF-, BMP- und TIFF-Format."
-- The lower end of the random pause interval. AI Studio never allows less than 6 seconds. -- The lower end of the random pause interval. AI Studio never allows less than 6 seconds.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "Das untere Ende des Intervalls für die zufällige Pause. AI Studio erlaubt niemals weniger als 6 Sekunden." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "Das untere Ende des Intervalls für die zufällige Pause. AI Studio erlaubt niemals weniger als 6 Sekunden."
@@ -8637,9 +8754,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T24
-- When enabled, new batch runs start with the defaults configured below. -- When enabled, new batch runs start with the defaults configured below.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "Wenn aktiviert, werden neue Stapel-Durchläufe mit den unten konfigurierten Standardwerten gestartet." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "Wenn aktiviert, werden neue Stapel-Durchläufe mit den unten konfigurierten Standardwerten gestartet."
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2594325620"] = "Trennen Sie mehrere Dateimuster durch ein Semikolon, z. B. *.pdf;*.docx. Die Standardmuster umfassen alle unterstützten Audio- und Videoformate."
-- Subfolders are included -- Subfolders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Unterordner werden einbezogen" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Unterordner werden einbezogen"
@@ -10374,6 +10488,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Fehlgeschlagene E-
-- Skipped -- Skipped
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Übersprungen" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Übersprungen"
-- Edit this data source
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T878700886"] = "Diese Datenquelle bearbeiten"
-- Open the transcription settings -- Open the transcription settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Transkriptionseinstellungen öffnen" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Transkriptionseinstellungen öffnen"
@@ -12210,18 +12327,39 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEM
-- Qdrant Edge is not available. -- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge ist nicht verfügbar." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge ist nicht verfügbar."
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T1215599168"] = "Datenquelle bearbeiten: Lokaler Ordner"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T221059217"] = "ERI v1 Datenquelle bearbeiten"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T2453292893"] = "Datenquelle bearbeiten: Lokale Datei"
-- Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T1555760974"] = "Jede andere Datei wird erneut an Ihren Einbettungsanbieter gesendet, was bei vielen Dateien eine Weile dauert. Dateien, die am neuen Speicherort fehlen, werden aus dem Index entfernt, während Ihre Dateien selbst unverändert bleiben."
-- AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2205693550"] = "Was AI Studio bereits vorbereitet hat, bleibt für jede Datei erhalten, die es am neuen Speicherort unverändert findet, also mit demselben Dateityp, derselben Größe und demselben Änderungsdatum. In einem Ordner muss eine solche Datei außerdem ihren Namen behalten und im selben Unterordner liegen."
-- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one. -- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "Sie beantworten weiterhin Stichwortsuchen, aber die Suche nach Bedeutung funktioniert nicht mehr, und es können keine weiteren Dokumente für sie vorbereitet werden. Bereits vorbereitete Datenquellen bleiben zudem an diesen Anbieter gebunden, sodass Sie sie nicht einfach auf einen anderen umstellen können." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "Sie beantworten weiterhin Stichwortsuchen, aber die Suche nach Bedeutung funktioniert nicht mehr, und es können keine weiteren Dokumente für sie vorbereitet werden. Bereits vorbereitete Datenquellen bleiben zudem an diesen Anbieter gebunden, sodass Sie sie nicht einfach auf einen anderen umstellen können."
-- and {0} more. -- and {0} more.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "und {0} weitere." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "und {0} weitere."
-- Your embedding provider runs in the cloud, so preparing files again costs money.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2635623258"] = "Ihr Einbettungsanbieter läuft in der Cloud, daher kostet es Geld, Dateien erneut vorzubereiten."
-- This change makes the prepared documents of the following data sources unusable ({0}): -- This change makes the prepared documents of the following data sources unusable ({0}):
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "Durch diese Änderung werden die vorbereiteten Dokumente der folgenden Datenquellen unbrauchbar ({0}):" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "Durch diese Änderung werden die vorbereiteten Dokumente der folgenden Datenquellen unbrauchbar ({0}):"
-- Do you want to apply this change anyway? -- Do you want to apply this change anyway?
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Möchten Sie diese Änderung trotzdem übernehmen?" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Möchten Sie diese Änderung trotzdem übernehmen?"
-- New Location for This Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3462449568"] = "Neuer Speicherort für diese Datenquelle"
-- Documents Will Be Prepared Again -- Documents Will Be Prepared Again
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Dokumente werden erneut vorbereitet" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Dokumente werden erneut vorbereitet"
@@ -12967,41 +13105,53 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T995107927"] = "Der Plugin-Code wurde nach der letzten Sicherheitsprüfung geändert. Das gespeicherte Ergebnis stimmt nicht mehr mit dem aktuellen Code überein, daher muss dieses Assistenten-Plugin erneut geprüft werden, bevor es aktiviert oder verwendet werden darf." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T995107927"] = "Der Plugin-Code wurde nach der letzten Sicherheitsprüfung geändert. Das gespeicherte Ergebnis stimmt nicht mehr mit dem aktuellen Code überein, daher muss dieses Assistenten-Plugin erneut geprüft werden, bevor es aktiviert oder verwendet werden darf."
-- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "Das Token oder Passwort der Datenquelle lässt sich auf diesem Computer nicht entschlüsseln. Datenquellen dieser Art können nur auf einem Computer importiert werden, auf dem das Verschlüsselungsgeheimnis Ihrer Organisation eingerichtet ist." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "Das Token oder Passwort der Datenquelle lässt sich auf diesem Rechner nicht entschlüsseln. Datenquellen dieser Art können nur auf einem Rechner importiert werden, auf dem das Verschlüsselungsgeheimnis Ihrer Organisation eingerichtet ist."
-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "Die Chat-Vorlage wählt den Anbieter „{0}“ vorab aus, der das für Chats erforderliche Vertrauensniveau nicht erfüllt. Mit ihr gestartete Chats verwenden dann Ihren üblichen Anbieter."
-- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Datenquellen, die Kerberos zur Anmeldung verwenden, können noch nicht importiert werden. Bitte fügen Sie sie selbst hinzu." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Datenquellen, die Kerberos zur Anmeldung verwenden, können noch nicht importiert werden. Bitte fügen Sie sie selbst hinzu."
-- The tokenizer '{0}' is not on this computer, so the provider is imported without it. -- The tokenizer '{0}' is not on this computer, so the provider is imported without it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2556276619"] = "Der Tokenizer „{0}“ ist auf diesem Computer nicht vorhanden. Daher wird der Anbieter ohne ihn importiert." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2556276619"] = "Der Tokenizer „{0}“ ist auf diesem Rechner nicht vorhanden. Daher wird der Anbieter ohne ihn importiert."
-- The chat template preselects data sources which do not exist on this computer: {0}. -- The chat template preselects data sources which do not exist on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2608225446"] = "Die Chat-Vorlage wählt Datenquellen vorab aus, die auf diesem Computer nicht vorhanden sind: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2608225446"] = "Die Chat-Vorlage wählt Datenquellen vorab aus, die auf diesem Rechner nicht vorhanden sind: {0}."
-- The chat template preselects tools which are not available on this computer: {0}. -- The chat template preselects tools which are not available on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "Die Chat-Vorlage wählt Werkzeuge vorab aus, die auf diesem Computer nicht verfügbar sind: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "Die Chat-Vorlage wählt Werkzeuge vorab aus, die auf diesem Rechner nicht verfügbar sind: {0}."
-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "Die Chat-Vorlage wählt ein Profil vorab aus, das auf diesem Rechner nicht vorhanden ist: {0}. Mit ihr gestartete Chats verwenden dann Ihr übliches Profil."
-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "Die Chat-Vorlage wählt einen Anbieter vorab aus, der auf diesem Rechner nicht vorhanden ist: {0}. Mit ihr gestartete Chats verwenden dann Ihren üblichen Anbieter."
-- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "Der API-Schlüssel in der eingefügten Konfiguration kann auf diesem Computer nicht entschlüsselt werden. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "Der API-Schlüssel in der eingefügten Konfiguration kann auf diesem Rechner nicht entschlüsselt werden. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern."
-- The chat template attaches these files from this computer to every chat started with it: {0}. Please check that you want to share them. -- The chat template attaches these files from this computer to every chat started with it: {0}. Please check that you want to share them.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3702015680"] = "Die Chat-Vorlage fügt jedem damit gestarteten Chat folgende Dateien von diesem Computer hinzu: {0}. Bitte vergewissern Sie sich, dass Sie diese Dateien teilen möchten." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3702015680"] = "Die Chat-Vorlage fügt jedem damit gestarteten Chat folgende Dateien von diesem Rechner hinzu: {0}. Bitte vergewissern Sie sich, dass Sie diese Dateien teilen möchten."
-- The policy preselects a provider which does not exist on this computer: {0}. -- The policy preselects a provider which does not exist on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3849646238"] = "Das Regelwerk wählt einen Anbieter vorab aus, der auf diesem Computer nicht vorhanden ist: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3849646238"] = "Das Regelwerk wählt einen Anbieter vorab aus, der auf diesem Rechner nicht vorhanden ist: {0}."
-- The pasted configuration is not a valid configuration of the type '{0}'. -- The pasted configuration is not a valid configuration of the type '{0}'.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T4231596123"] = "Die eingefügte Konfiguration ist keine gültige Konfiguration vom Typ „{0}“." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T4231596123"] = "Die eingefügte Konfiguration ist keine gültige Konfiguration vom Typ „{0}“."
-- The policy permits tools which are not available on this computer: {0}. -- The policy permits tools which are not available on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T582643438"] = "Das Regelwerk erlaubt Werkzeuge, die auf diesem Computer nicht verfügbar sind: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T582643438"] = "Das Regelwerk erlaubt Werkzeuge, die auf diesem Rechner nicht verfügbar sind: {0}."
-- The policy preselects a profile which does not exist on this computer: {0}. -- The policy preselects a profile which does not exist on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600255824"] = "Das Regelwerk wählt ein Profil vorab aus, das auf diesem Computer nicht vorhanden ist: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600255824"] = "Das Regelwerk wählt ein Profil vorab aus, das auf diesem Rechner nicht vorhanden ist: {0}."
-- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "Die eingefügte Konfiguration enthält einen unverschlüsselten API-Schlüssel. Dieser wurde daher ausgelassen. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "Die eingefügte Konfiguration enthält einen unverschlüsselten API-Schlüssel. Dieser wurde daher ausgelassen. Bitte geben Sie Ihren API-Schlüssel ein, bevor Sie den Anbieter speichern."
-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "Die Chat-Vorlage wählt vorab ein Profil aus, erlaubt aber keine Profile. Mit ihr gestartete Chats ignorieren dieses Profil."
-- Expected a quoted string at character {0}. -- Expected a quoted string at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "An Position {0} wurde ein Text in Anführungszeichen erwartet." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "An Position {0} wurde ein Text in Anführungszeichen erwartet."
@@ -13404,9 +13554,36 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source Code
-- Document -- Document
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Dokument"
-- Image for AI models
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T791951937"] = "Bild für KI-Modelle"
-- Plugin archive -- Plugin archive
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin-Archiv" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin-Archiv"
-- AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1345506748"] = "AI Studio konnte das Bild „{0}“ nicht für die KI vorbereiten und hat es daher nicht gesendet."
-- The image '{0}' could not be made small enough for the selected provider, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1463083567"] = "Das Bild „{0}“ konnte für den ausgewählten Anbieter nicht ausreichend verkleinert werden und wurde daher nicht gesendet."
-- The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2022724454"] = "Das Bild „{0}“ konnte nicht geöffnet und daher nicht gesendet werden. Wenn das Bild auf einem Netzlaufwerk gespeichert ist, ist das Laufwerk möglicherweise nicht verfügbar."
-- The image '{0}' does not exist anymore and was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2330229240"] = "Das Bild „{0}“ existiert nicht mehr und wurde nicht gesendet."
-- The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T3457568536"] = "Das Bild „{0}“ hat nicht das Format, das seine Dateiendung angibt, und wurde daher nicht gesendet. Bitte korrigieren Sie die Dateiendung."
-- The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T4029166607"] = "Das Bild „{0}“ konnte nicht gelesen und daher nicht gesendet werden. Möglicherweise ist es beschädigt oder unvollständig übertragen worden."
-- The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T567941882"] = "Das Bild „{0}“ hat zu viele Pixel, als dass AI Studio es vorbereiten könnte, und wurde daher nicht gesendet. Bitte verkleinern Sie das Bild zuerst."
-- The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T568830141"] = "Das Bild „{0}“ liegt in einem Format vor, das AI Studio nicht an ein KI-Modell übergeben kann, und wurde daher nicht gesendet. Bitte konvertieren Sie das Bild in JPG oder PNG."
-- Attempt to override instructions -- Attempt to override instructions
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Versuch, Anweisungen zu überschreiben" UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Versuch, Anweisungen zu überschreiben"
@@ -13761,18 +13938,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T8042
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "Der Ordner „{0}“ konnte nicht geöffnet werden. Bitte prüfen Sie, ob Sie die erforderlichen Leserechte haben. Aus dem Index wurde nichts entfernt." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "Der Ordner „{0}“ konnte nicht geöffnet werden. Bitte prüfen Sie, ob Sie die erforderlichen Leserechte haben. Aus dem Index wurde nichts entfernt."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1393648320"] = "Die Datei „{0}“ ist nicht erreichbar. Möglicherweise ist das Laufwerk, auf dem sie sich befindet, nicht verbunden, oder die Datei wurde verschoben oder umbenannt. Aus dem Index wurde nichts entfernt. Sobald die Datei wieder verfügbar ist, aktualisieren Sie diese Datenquelle. Wenn sie dauerhaft nicht mehr vorhanden ist, entfernen Sie diese Datenquelle."
-- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run. -- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "Die Datei „{0}“ wurde während der Indexierung geändert. Die bereits indexierten Inhalte werden verworfen, und die Datei wird beim nächsten Durchlauf erneut verarbeitet." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "Die Datei „{0}“ wurde während der Indexierung geändert. Die bereits indexierten Inhalte werden verworfen, und die Datei wird beim nächsten Durchlauf erneut verarbeitet."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2268993048"] = "Die Datei „{0}“ ist nicht erreichbar. Möglicherweise ist das Laufwerk, auf dem sie sich befindet, nicht verbunden, oder die Datei wurde verschoben oder umbenannt. Aus dem Index wurde nichts entfernt. Wenn die Datei verschoben oder umbenannt wurde, bearbeiten Sie diese Datenquelle und wählen Sie den neuen Speicherort der Datei aus. Sobald die Datei wieder verfügbar ist, aktualisieren Sie diese Datenquelle. Wenn sie dauerhaft nicht mehr vorhanden ist, entfernen Sie diese Datenquelle."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2692299965"] = "Der Ordner „{0}“ ist nicht erreichbar. Möglicherweise ist das Laufwerk, auf dem er sich befindet, nicht verbunden, oder der Ordner wurde verschoben oder umbenannt. Aus dem Index wurde nichts entfernt. Wenn der Ordner verschoben oder umbenannt wurde, bearbeiten Sie diese Datenquelle und wählen Sie den neuen Speicherort des Ordners aus. Sobald der Ordner wieder verfügbar ist, aktualisieren Sie diese Datenquelle."
-- The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2999277901"] = "Der Ordner „{0}“ scheint derzeit leer zu sein. AI Studio hat den Index dieser Datenquelle beibehalten, da ein Ordner auf einem nicht verbundenen Laufwerk leer erscheinen kann. Verbinden Sie das Laufwerk und aktualisieren Sie diese Datenquelle. Wenn Sie den Ordner absichtlich geleert haben, entfernen Sie diese Datenquelle."
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "Der Ordner „{0}“ konnte nicht geöffnet werden. Bitte prüfen Sie, ob Sie ihn lesen dürfen." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "Der Ordner „{0}“ konnte nicht geöffnet werden. Bitte prüfen Sie, ob Sie ihn lesen dürfen."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3722338131"] = "Der Ordner „{0}“ ist nicht erreichbar. Möglicherweise ist das Laufwerk, auf dem er sich befindet, nicht verbunden, oder der Ordner wurde verschoben oder umbenannt. Aus dem Index wurde nichts entfernt. Sobald der Ordner wieder verfügbar ist, aktualisieren Sie diese Datenquelle."
-- The file '{0}' could not be read. Please check whether you are allowed to read it. -- The file '{0}' could not be read. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "Die Datei „{0}“ konnte nicht gelesen werden. Bitte prüfen Sie, ob Sie berechtigt sind, sie zu lesen." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "Die Datei „{0}“ konnte nicht gelesen werden. Bitte prüfen Sie, ob Sie berechtigt sind, sie zu lesen."
@@ -14634,11 +14814,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"]
-- Please enter the secret necessary for authentication. -- Please enter the secret necessary for authentication.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Bitte geben Sie das für die Authentifizierung benötigte Geheimnis ein." UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Bitte geben Sie das für die Authentifizierung benötigte Geheimnis ein."
-- Unsupported image format -- AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1398282880"] = "Nicht unterstütztes Bildformat" UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1578559061"] = "AI Studio kann keine Bilder im Format {0} an ein KI-Modell übergeben. Bitte konvertieren Sie das Bild in JPG oder PNG."
-- File has no extension
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1555980031"] = "Datei hat keine Erweiterung"
-- Audio files are not supported yet -- Audio files are not supported yet
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio-Dateien werden noch nicht unterstützt." UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio-Dateien werden noch nicht unterstützt."
@@ -426,6 +426,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The selected folder does not exist. -- The selected folder does not exist.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "The selected folder does not exist." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T17705912"] = "The selected folder does not exist."
-- The image was no longer available while the model was asked about it.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1805791865"] = "The image was no longer available while the model was asked about it."
-- Minimum pause between files -- Minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Minimum pause between files" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T1829787634"] = "Minimum pause between files"
@@ -531,9 +534,15 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Was not able to write the result file: {0} -- Was not able to write the result file: {0}
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T2991295581"] = "Was not able to write the result file: {0}"
-- Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3011193639"] = "Images in JPG, PNG, WebP, GIF, BMP, or TIFF format are given to the model as images, so you need a model that understands images. If the selected model cannot read images, remove the image patterns, such as *.jpg or *.png, before you start."
-- The batch run finished, but one file could not be processed. See the progress table and log for details. -- The batch run finished, but one file could not be processed. See the progress table and log for details.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3201532790"] = "The batch run finished, but one file could not be processed. See the progress table and log for details."
-- The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T324865195"] = "The selected model cannot read images, so the {0} images among the selected files will fail. All other files are processed as usual."
-- Maximum pause between files -- Maximum pause between files
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximum pause between files" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3250003796"] = "Maximum pause between files"
@@ -552,6 +561,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- Show Expert Settings -- Show Expert Settings
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Show Expert Settings" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3361153305"] = "Show Expert Settings"
-- All selected files are images, but the selected model cannot read images. Select a model with image support.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3365102967"] = "All selected files are images, but the selected model cannot read images. Select a model with image support."
-- Header of the result column (optional) -- Header of the result column (optional)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Header of the result column (optional)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T340994102"] = "Header of the result column (optional)"
@@ -564,6 +576,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'. -- The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3439247329"] = "The file name of the CSV results table. We add the ending .csv when it is missing. When left empty, we use 'batch-results.csv'."
-- The selected model cannot read images. Select a model with image support to process this image.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T346986514"] = "The selected model cannot read images. Select a model with image support to process this image."
-- Document analysis policy -- Document analysis policy
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Document analysis policy" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T3510564924"] = "Document analysis policy"
@@ -678,6 +693,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING
-- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here. -- The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here." UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T967206794"] = "The AI may use these tools while working on each document. Every tool has to meet the confidence requirements of the selected provider, so a tool may stay unavailable even when it is selected here."
-- The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::ASSISTANTBATCHPROCESSING::T9738678"] = "The selected model cannot read images, so the image among the selected files will fail. All other files are processed as usual."
-- Comma (,) -- Comma (,)
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Comma (,)" UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::BATCHPROCESSING::BATCHPROCESSINGCSVSEPARATOREXTENSIONS::T1676507543"] = "Comma (,)"
@@ -4122,9 +4140,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1084943026"] = "Yo
-- Automatic local data source refresh -- Automatic local data source refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1208397349"] = "Automatic local data source refresh"
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T1215599168"] = "Edit Local Directory Data Source"
-- Refresh -- Refresh
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T135637716"] = "Refresh"
@@ -4152,12 +4167,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2067030738"] = "Ar
-- Local Directory Data Source Information -- Local Directory Data Source Information
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2146756020"] = "Local Directory Data Source Information"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T221059217"] = "Edit ERI v1 Data Source"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2453292893"] = "Edit Local File Data Source"
-- Local data sources refresh when files change, mailboxes every 16 minutes. -- Local data sources refresh when files change, mailboxes every 16 minutes.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T2511157475"] = "Local data sources refresh when files change, mailboxes every 16 minutes."
@@ -4392,9 +4401,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1797236585"] = "
-- Workspace name (Optional) -- Workspace name (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1873204484"] = "Workspace name (Optional)"
-- The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1876510820"] = "The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats."
-- Use no profile -- Use no profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile"
-- The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2344103917"] = "The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."
-- Existing workspace (Optional) -- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)"
@@ -4416,6 +4431,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2901190527"] = "
-- Data sources (Optional) -- Data sources (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)" UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)"
-- The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3419242912"] = "The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists."
-- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings. -- Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings." UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3611496116"] = "Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings."
@@ -6180,12 +6198,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options. -- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options."
-- Preselected provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1469984996"] = "Preselected provider"
-- Please enter a name for the chat template. -- Please enter a name for the chat template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
-- A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T171066641"] = "A chat template may decide which provider, and with it which model, a chat starts with. Without such a decision, those chats start with the provider they would start with anyway. The provider stays changeable in the chat, and it must meet the confidence level required for chats."
-- Yes, this template decides which data a chat starts with -- Yes, this template decides which data a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T17861006"] = "Yes, this template decides which data a chat starts with"
-- Preselected profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T178965494"] = "Preselected profile"
-- Enter the full path of a file on this computer, or remove the attachment. -- Enter the full path of a file on this computer, or remove the attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1798848159"] = "Enter the full path of a file on this computer, or remove the attachment."
@@ -6195,12 +6222,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1837026610"] = "Load pr
-- Update -- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1847791252"] = "Update"
-- Yes, this template decides which profile a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1865005833"] = "Yes, this template decides which profile a chat starts with"
-- The chat template name must not exceed 40 characters. -- The chat template name must not exceed 40 characters.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat template name must not exceed 40 characters."
-- Profile Usage -- Profile Usage
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage"
-- Use no profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205839602"] = "Use no profile"
-- Yes, chats with this template go into a workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2205963533"] = "Yes, chats with this template go into a workspace"
-- Relink attachment: {0} -- Relink attachment: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}"
@@ -6210,6 +6246,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add mes
-- File Attachments -- File Attachments
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2294745309"] = "File Attachments"
-- No, chats go where they would go anyway
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2355334446"] = "No, chats go where they would go anyway"
-- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2364306588"] = "Existing workspace (Optional)"
-- Role -- Role
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2418769465"] = "Role"
@@ -6222,6 +6264,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2499909372"] = "Tools"
-- What predefined user input do you want to use? -- What predefined user input do you want to use?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2501284417"] = "What predefined user input do you want to use?"
-- Please enter the workspace for the chats of this template, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2504276031"] = "Please enter the workspace for the chats of this template, or switch the preselection off."
-- Does this chat template preselect a provider?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2541393632"] = "Does this chat template preselect a provider?"
-- Tell the AI your system prompt. -- Tell the AI your system prompt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2552381261"] = "Tell the AI your system prompt."
@@ -6231,6 +6279,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro
-- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with. -- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with."
-- Does this chat template preselect a profile?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2602340928"] = "Does this chat template preselect a profile?"
-- Predefined User Input -- Predefined User Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input"
@@ -6246,9 +6297,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2719480499"] = "The mes
-- Chat Template Name -- Chat Template Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T275026390"] = "Chat Template Name"
-- Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2828180414"] = "Workspaces are switched off in your workspace settings. While they are off, chats with this template belong to no workspace, and they do not move into a workspace when you switch workspaces on again. You can change this choice once workspaces are on."
-- Please relink the attachment '{0}' to a file on this computer or remove it before you save. -- Please relink the attachment '{0}' to a file on this computer or remove it before you save.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2884684079"] = "Please relink the attachment '{0}' to a file on this computer or remove it before you save."
-- Workspace name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T295876489"] = "Workspace name"
-- Commit Changes -- Commit Changes
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2996302517"] = "Commit Changes"
@@ -6264,21 +6321,42 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3108503534"] = "You can
-- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats. -- Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you unsure which system prompt to use? You might start with the default system prompt that AI Studio uses for all chats."
-- A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T313529011"] = "A chat template may also decide which profile a chat starts with, including starting without one. Without such a decision, those chats start with the profile they would start with anyway. The profile stays changeable in the chat."
-- The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3170335831"] = "The profile this chat template preselects does not exist on this computer. Chats started with it keep their usual profile."
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here. -- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here."
-- Choose an existing workspace or enter a name that should be created when a chat with this template starts.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3256439227"] = "Choose an existing workspace or enter a name that should be created when a chat with this template starts."
-- No, chats keep the data source options from your chat options -- No, chats keep the data source options from your chat options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options"
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says. -- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
-- A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3352941260"] = "A chat template may keep its chats in a workspace of its own, which is created when it does not exist yet. This applies whenever a chat would otherwise be a disappearing chat, for example, when you start a new chat outside the workspaces or pick this template in a new chat. A chat you start in a workspace on purpose, for example, from the workspace list, stays in that workspace."
-- Add a message -- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
-- Yes, this template decides which provider a chat starts with
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3422337353"] = "Yes, this template decides which provider a chat starts with"
-- Close -- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- No, chats keep their usual profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3465925873"] = "No, chats keep their usual profile"
-- The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T349281189"] = "The provider this chat template preselects does not exist on this computer or does not meet the confidence level required for chats. Chats started with it keep their usual provider."
-- Unsupported content type -- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
@@ -6315,12 +6393,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4051106111"] = "Use the
-- Tell the AI your predefined user input. -- Tell the AI your predefined user input.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4052406705"] = "Tell the AI your predefined user input."
-- Does this chat template keep its chats in a workspace?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4057825866"] = "Does this chat template keep its chats in a workspace?"
-- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template. -- Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4194591607"] = "Some attachments of this chat template are not on this computer. Please relink or remove them before you add the template."
-- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements. -- Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create your custom chat template to tailor the LLM's behavior for specific tasks or domains. Define a custom system prompt and provide an example conversation to design an AI experience perfectly suited to your requirements."
-- Please choose the provider this chat template preselects, or switch the preselection off.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T442309864"] = "Please choose the provider this chat template preselects, or switch the preselection off."
-- Enter a message -- Enter a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
@@ -6333,15 +6417,24 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself. -- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
-- Workspace
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T658612054"] = "Workspace"
-- Remove this attachment -- Remove this attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment"
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use. -- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
-- No, chats keep their usual provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T787085105"] = "No, chats keep their usual provider"
-- Allow the use of profiles together with this chat template? -- Allow the use of profiles together with this chat template?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T823785464"] = "Allow the use of profiles together with this chat template?"
-- Provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900237532"] = "Provider"
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T900713019"] = "Cancel"
@@ -6807,9 +6900,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2460883298"
-- Add -- Add
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Add" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2646845972"] = "Add"
-- The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2686150329"] = "The documents of this data source are already prepared, so its folder cannot be changed. Another folder holds other documents, which makes it another data source: please add one for it. The embedding provider below can be changed."
-- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud. -- The embedding you selected runs locally or in your organization. Your data is not sent to the cloud.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "The embedding you selected runs locally or in your organization. Your data is not sent to the cloud." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T2814869210"] = "The embedding you selected runs locally or in your organization. Your data is not sent to the cloud."
@@ -7068,9 +7158,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T787118522"] = "T
-- Data Source Name -- Data Source Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Data Source Name" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T813773421"] = "Data Source Name"
-- The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T890383764"] = "The documents of this data source are already prepared, so its file cannot be changed. Another file holds other content, which makes it another data source: please add one for it. The embedding provider below can be changed."
-- Cancel -- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Cancel" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALFILEDIALOG::T900713019"] = "Cancel"
@@ -8580,6 +8667,33 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T6790
-- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model. -- When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T711745239"] = "When enabled, you can preselect options. This is might be useful when you prefer a specific language or LLM model."
-- When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T1496952909"] = "When enabled, the Assistant Builder starts with the defaults configured below. When disabled, it uses the app-wide default provider and preselects no output language."
-- Preselect Assistant Builder options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T2127606148"] = "Preselect Assistant Builder options?"
-- Default custom output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3360584376"] = "Default custom output language"
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3448155331"] = "Close"
-- Default output language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3652922567"] = "Default output language"
-- Assistant: Assistant Builder defaults
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3668274570"] = "Assistant: Assistant Builder defaults"
-- Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T3962364100"] = "Assistant Builder options are preselected"
-- No Assistant Builder options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T4104323209"] = "No Assistant Builder options are preselected"
-- The language in which the assistants you build answer.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBUILDER::T42126731"] = "The language in which the assistants you build answer."
-- Default minimum pause between files -- Default minimum pause between files
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Default minimum pause between files" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T1008440099"] = "Default minimum pause between files"
@@ -8625,6 +8739,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T22
-- Hidden files and folders are included -- Hidden files and folders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Hidden files and folders are included" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2288334048"] = "Hidden files and folders are included"
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T232588799"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats as well as images in JPG, PNG, WebP, GIF, BMP, and TIFF format."
-- The lower end of the random pause interval. AI Studio never allows less than 6 seconds. -- The lower end of the random pause interval. AI Studio never allows less than 6 seconds.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "The lower end of the random pause interval. AI Studio never allows less than 6 seconds." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T237774509"] = "The lower end of the random pause interval. AI Studio never allows less than 6 seconds."
@@ -8637,9 +8754,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T24
-- When enabled, new batch runs start with the defaults configured below. -- When enabled, new batch runs start with the defaults configured below.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below." UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2592677194"] = "When enabled, new batch runs start with the defaults configured below."
-- Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2594325620"] = "Separate multiple file patterns with a semicolon, e.g., *.pdf;*.docx. The standard patterns include all supported audio and video formats."
-- Subfolders are included -- Subfolders are included
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Subfolders are included" UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T2607092632"] = "Subfolders are included"
@@ -10374,6 +10488,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T819555436"] = "Failed mails: {0}"
-- Skipped -- Skipped
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T852993367"] = "Skipped"
-- Edit this data source
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T878700886"] = "Edit this data source"
-- Open the transcription settings -- Open the transcription settings
UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings" UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T905182399"] = "Open the transcription settings"
@@ -12210,18 +12327,39 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEM
-- Qdrant Edge is not available. -- Qdrant Edge is not available.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available."
-- Edit Local Directory Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T1215599168"] = "Edit Local Directory Data Source"
-- Edit ERI v1 Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T221059217"] = "Edit ERI v1 Data Source"
-- Edit Local File Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEEDITING::T2453292893"] = "Edit Local File Data Source"
-- Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T1555760974"] = "Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are."
-- AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2205693550"] = "AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in."
-- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one. -- They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2343773457"] = "They keep answering keyword searches, but searching them by meaning stops working, and no further documents can be prepared for them. The ones which are already prepared stay tied to this provider as well, so you cannot simply move them to another one."
-- and {0} more. -- and {0} more.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "and {0} more." UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2519847121"] = "and {0} more."
-- Your embedding provider runs in the cloud, so preparing files again costs money.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T2635623258"] = "Your embedding provider runs in the cloud, so preparing files again costs money."
-- This change makes the prepared documents of the following data sources unusable ({0}): -- This change makes the prepared documents of the following data sources unusable ({0}):
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "This change makes the prepared documents of the following data sources unusable ({0}):" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3337378891"] = "This change makes the prepared documents of the following data sources unusable ({0}):"
-- Do you want to apply this change anyway? -- Do you want to apply this change anyway?
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Do you want to apply this change anyway?" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3419411838"] = "Do you want to apply this change anyway?"
-- New Location for This Data Source
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T3462449568"] = "New Location for This Data Source"
-- Documents Will Be Prepared Again -- Documents Will Be Prepared Again
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Documents Will Be Prepared Again" UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATASOURCEREINDEXWARNING::T737291513"] = "Documents Will Be Prepared Again"
@@ -12969,6 +13107,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
-- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured. -- The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T186946778"] = "The token or password of the data source cannot be decrypted on this computer. Such data sources can only be imported where the encryption secret of your organization is configured."
-- The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T190892810"] = "The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead."
-- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself. -- Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T206419407"] = "Data sources which sign in with Kerberos cannot be imported yet. Please add them yourself."
@@ -12981,6 +13122,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T260
-- The chat template preselects tools which are not available on this computer: {0}. -- The chat template preselects tools which are not available on this computer: {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2693686582"] = "The chat template preselects tools which are not available on this computer: {0}."
-- The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T2735390326"] = "The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead."
-- The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3321789654"] = "The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead."
-- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider. -- The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T3589070261"] = "The API key in the pasted configuration cannot be decrypted on this computer. Please enter your API key before you save the provider."
@@ -13002,6 +13149,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T600
-- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider. -- The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T639320638"] = "The pasted configuration contains an API key which is not encrypted, so it was left out. Please enter your API key before you save the provider."
-- The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORT::T769062029"] = "The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile."
-- Expected a quoted string at character {0}. -- Expected a quoted string at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}." UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}."
@@ -13404,9 +13554,36 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T378481461"] = "Source like p
-- Document -- Document
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T4165204724"] = "Document"
-- Image for AI models
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T791951937"] = "Image for AI models"
-- Plugin archive -- Plugin archive
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive" UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::FILETYPES::T927001356"] = "Plugin archive"
-- AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1345506748"] = "AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent."
-- The image '{0}' could not be made small enough for the selected provider, so it was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T1463083567"] = "The image '{0}' could not be made small enough for the selected provider, so it was not sent."
-- The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2022724454"] = "The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable."
-- The image '{0}' does not exist anymore and was not sent.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T2330229240"] = "The image '{0}' does not exist anymore and was not sent."
-- The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T3457568536"] = "The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension."
-- The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T4029166607"] = "The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely."
-- The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T567941882"] = "The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first."
-- The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::RUST::MODELIMAGEERRORCODEEXTENSIONS::T568830141"] = "The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG."
-- Attempt to override instructions -- Attempt to override instructions
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Attempt to override instructions" UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONFINDINGCATEGORYEXTENSIONS::T161976090"] = "Attempt to override instructions"
@@ -13761,18 +13938,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::DOCUMENTRUNPROGRESS::T8042
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1214987337"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1393648320"] = "The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source."
-- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run. -- The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T1935191670"] = "The file '{0}' changed while it was being indexed. What was indexed of it is discarded, and the file is tried again during the next run."
-- The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2268993048"] = "The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2692299965"] = "The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source."
-- The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T2999277901"] = "The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source."
-- The folder '{0}' could not be opened. Please check whether you are allowed to read it. -- The folder '{0}' could not be opened. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3230000698"] = "The folder '{0}' could not be opened. Please check whether you are allowed to read it."
-- The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3722338131"] = "The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source."
-- The file '{0}' could not be read. Please check whether you are allowed to read it. -- The file '{0}' could not be read. Please check whether you are allowed to read it.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "The file '{0}' could not be read. Please check whether you are allowed to read it." UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::INDEXING::FILESOURCEINDEXER::T3924882233"] = "The file '{0}' could not be read. Please check whether you are allowed to read it."
@@ -14634,11 +14814,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T90753429"]
-- Please enter the secret necessary for authentication. -- Please enter the secret necessary for authentication.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication." UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::DATASOURCEVALIDATION::T968385876"] = "Please enter the secret necessary for authentication."
-- Unsupported image format -- AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1398282880"] = "Unsupported image format" UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1578559061"] = "AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG."
-- File has no extension
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T1555980031"] = "File has no extension"
-- Audio files are not supported yet -- Audio files are not supported yet
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio files are not supported yet" UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T2919730669"] = "Audio files are not supported yet"
+1
View File
@@ -175,6 +175,7 @@ internal sealed class Program
builder.Services.AddSingleton<SettingsManager>(); builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<AIGeneratedContentDisclosureService>(); builder.Services.AddSingleton<AIGeneratedContentDisclosureService>();
builder.Services.AddSingleton<PromptInjectionGuardService>(); builder.Services.AddSingleton<PromptInjectionGuardService>();
builder.Services.AddSingleton<ModelImageService>();
builder.Services.AddSingleton<ToolSettingsService>(); builder.Services.AddSingleton<ToolSettingsService>();
builder.Services.AddSingleton<WebPageRetrievalService>(); builder.Services.AddSingleton<WebPageRetrievalService>();
builder.Services.AddSingleton<IToolImplementation, ReadWebPageTool>(); builder.Services.AddSingleton<IToolImplementation, ReadWebPageTool>();
@@ -59,18 +59,16 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, n
Text = text, Text = text,
}, },
// Anthropic-specific image sub-content: // Anthropic-specific image sub-content, which takes the base64 and its type apart:
async attachment => new SubContentImage image => new SubContentImage
{ {
Source = new SubContentBase64Image Source = new SubContentBase64Image
{ {
Data = await attachment.TryAsBase64(token: token) is (true, var base64Content) Data = image.Base64,
? base64Content MediaType = image.MimeType,
: string.Empty,
MediaType = attachment.DetermineMimeType(),
}
} }
},
token
); );
// //
@@ -288,6 +288,27 @@ public static class LLMProvidersExtensions
_ => false, _ => false,
}; };
/// <summary>
/// The largest image file, in bytes, which the provider accepts in a request, where it states one.
/// </summary>
/// <remarks>
/// The limit belongs to the API of the provider, not to the model: the same model may accept
/// larger images through another provider. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit.
/// </remarks>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>The size limit of one image file, or null when the provider states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this LLMProviders llmProvider) => llmProvider switch
{
//
// Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four
// characters for every three bytes, which leaves 3,932,160 bytes for the image file itself:
//
LLMProviders.ANTHROPIC => 3_932_160,
_ => null,
};
/// <summary> /// <summary>
/// Creates a new provider instance based on the provider value. /// Creates a new provider instance based on the provider value.
/// </summary> /// </summary>
@@ -113,15 +113,14 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
{ {
Text = text, Text = text,
}, },
async attachment => new SubContentImageUrlNested image => new SubContentImageUrlNested
{ {
ImageUrl = new SubContentImageUrlData ImageUrl = new SubContentImageUrlData
{ {
Url = await attachment.TryAsBase64(token: token) is (true, var base64Content) Url = image.ToDataUrl(),
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}"
: string.Empty,
}, },
}); },
token);
return new ChatCompletionAPIRequest return new ChatCompletionAPIRequest
{ {
@@ -192,12 +191,11 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, new Ur
{ {
Text = text, Text = text,
}, },
async attachment => new SubContentInputImage image => new SubContentInputImage
{ {
ImageUrl = await attachment.TryAsBase64(token: token) is (true, var base64Content) ImageUrl = image.ToDataUrl(),
? $"data:{attachment.DetermineMimeType()};base64,{base64Content}" },
: string.Empty, token);
});
var baseInput = new List<object> { systemPrompt }; var baseInput = new List<object> { systemPrompt };
baseInput.AddRange(messages); baseInput.AddRange(messages);
@@ -36,6 +36,29 @@ public static class HostExtensions
_ => "embeddings", _ => "embeddings",
}; };
/// <summary>
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
/// </summary>
/// <remarks>
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <param name="host">The engine.</param>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this Host host) => host switch
{
//
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
// keeps the default limit of its web framework, read on 2026-10-22 at
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
//
Host.LLMMAN => 1_048_576,
_ => null,
};
public static bool IsChatSupported(this Host host) public static bool IsChatSupported(this Host host)
{ {
switch (host) switch (host)
+135 -11
View File
@@ -51,6 +51,46 @@ public record ChatTemplate(
/// </remarks> /// </remarks>
public DataSourceOptions? DataSourceOptions { get; init; } public DataSourceOptions? DataSourceOptions { get; init; }
/// <summary>
/// The profile this template preselects for a chat started with it.
/// </summary>
/// <remarks>
/// Null means the template says nothing about profiles, so the chat keeps the profile it would
/// start with anyway: the default of the chat settings, or the one the user picked. The ID of
/// Profile.NO_PROFILE is a statement of its own: such chats start without a profile, whatever
/// that default says.<br/><br/>
/// A preselection, not a limit: the user picks another profile in the chat as usual. It means
/// nothing for a template which does not allow profiles, and a profile which is gone leaves the
/// chat default in place.
/// </remarks>
public string? ProfileId { get; init; }
/// <summary>
/// The provider, and with it the model, this template preselects for a chat started with it.
/// </summary>
/// <remarks>
/// Null means the template says nothing, so the chat starts with the provider it would start
/// with anyway. A preselection, not a limit, and no way around the confidence levels either: a
/// provider which is gone, or which may not be used for chats, leaves the chat to start as if
/// the template said nothing.
/// </remarks>
public string? ProviderId { get; init; }
/// <summary>
/// The name of the workspace a chat started with this template is kept in.
/// </summary>
/// <remarks>
/// Null means the template says nothing, so the chat goes where it would go anyway. A name
/// rather than an ID: a workspace has an ID of its own on every computer, so a configuration
/// plugin has no other way to name one. Like for an assistant chat launcher, the workspace is
/// created when it does not exist yet; on the chat page only with the first message of the chat,
/// so that trying out a template creates no workspace.<br/><br/>
/// It only fills in where a chat would belong to no workspace otherwise. A chat started in a
/// workspace on purpose, for example from the workspace list, stays there, and so does every
/// chat while workspaces are switched off.
/// </remarks>
public string? WorkspaceName { get; init; }
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatTemplate).Namespace, nameof(ChatTemplate));
private static readonly ILogger<ChatTemplate> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatTemplate>(); private static readonly ILogger<ChatTemplate> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ChatTemplate>();
@@ -67,6 +107,9 @@ public record ChatTemplate(
AllowProfileUsage = true, AllowProfileUsage = true,
ToolIds = null, ToolIds = null,
DataSourceOptions = null, DataSourceOptions = null,
ProfileId = null,
ProviderId = null,
WorkspaceName = null,
EnterpriseConfigurationPluginId = Guid.Empty, EnterpriseConfigurationPluginId = Guid.Empty,
IsEnterpriseConfiguration = false, IsEnterpriseConfiguration = false,
}; };
@@ -81,6 +124,16 @@ public record ChatTemplate(
#endregion #endregion
/// <summary>
/// Whether this template starts its chats without a profile, whatever the default says.
/// </summary>
/// <remarks>
/// That is a statement of its own, made with the ID of the no-profile entry, see ProfileId. It
/// names no profile which could be missing on another computer. A method rather than a property,
/// so that it does not end up in the stored settings.
/// </remarks>
public bool StartsWithoutProfile() => string.Equals(this.ProfileId, Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase);
/// <summary> /// <summary>
/// Gets the name of this chat template. If it is the NO_CHAT_TEMPLATE, it returns a localized string. /// Gets the name of this chat template. If it is the NO_CHAT_TEMPLATE, it returns a localized string.
/// </summary> /// </summary>
@@ -117,12 +170,12 @@ public record ChatTemplate(
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param> /// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
/// <param name="launcherToolIds">The tools the launcher names itself, or null when it names none.</param> /// <param name="launcherToolIds">The tools the launcher names itself, or null when it names none.</param>
/// <returns>The tools to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for it.</returns> /// <returns>The tools to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for it.</returns>
public static (IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped) ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds) public static ChatTemplateToolChoice ChooseToolIds(ChatTemplate chatTemplate, IReadOnlyCollection<string>? launcherToolIds)
{ {
if (chatTemplate.ToolIds is not { } templateToolIds) if (chatTemplate.ToolIds is not { } templateToolIds)
return (launcherToolIds, false); return new(launcherToolIds, false);
return (templateToolIds, launcherToolIds is not null); return new(templateToolIds, launcherToolIds is not null);
} }
/// <summary> /// <summary>
@@ -137,12 +190,12 @@ public record ChatTemplate(
/// <param name="chatTemplate">The chat template the launcher opens its chat with.</param> /// <param name="chatTemplate">The chat template the launcher opens its chat with.</param>
/// <param name="launcherOptions">The options built from the data sources the launcher names, or null when it names none.</param> /// <param name="launcherOptions">The options built from the data sources the launcher names, or null when it names none.</param>
/// <returns>The options to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for them.</returns> /// <returns>The options to start with — null when neither says anything, which leaves the chat default in place — and whether the launcher's own choice was dropped for them.</returns>
public static (DataSourceOptions? Options, bool LauncherChoiceDropped) ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions) public static ChatTemplateDataSourceChoice ChooseDataSourceOptions(ChatTemplate chatTemplate, DataSourceOptions? launcherOptions)
{ {
if (chatTemplate.DataSourceOptions is not { } templateOptions) if (chatTemplate.DataSourceOptions is not { } templateOptions)
return (launcherOptions, false); return new(launcherOptions, false);
return (templateOptions.CreateCopy(), launcherOptions is not null); return new(templateOptions.CreateCopy(), launcherOptions is not null);
} }
/// <summary> /// <summary>
@@ -202,6 +255,10 @@ public record ChatTemplate(
var fileAttachments = ParseFileAttachments(idx, table, pluginPath); var fileAttachments = ParseFileAttachments(idx, table, pluginPath);
var profileId = ParseOptionalId(idx, table, "ProfileId", allowEmpty: true);
if (profileId is not null && !allowProfileUsage)
LOGGER.LogWarning("The chat template {IdxChatTemplate} preselects a profile but does not allow profiles. Chats started with it ignore that profile.", idx);
template = new ChatTemplate template = new ChatTemplate
{ {
Num = 0, // will be set later by the PluginConfigurationObject Num = 0, // will be set later by the PluginConfigurationObject
@@ -214,6 +271,9 @@ public record ChatTemplate(
AllowProfileUsage = allowProfileUsage, AllowProfileUsage = allowProfileUsage,
ToolIds = ParseToolIds(idx, table), ToolIds = ParseToolIds(idx, table),
DataSourceOptions = ParseDataSourceOptions(idx, table), DataSourceOptions = ParseDataSourceOptions(idx, table),
ProfileId = profileId,
ProviderId = ParseOptionalId(idx, table, "ProviderId", allowEmpty: false),
WorkspaceName = ParseWorkspaceName(idx, table),
IsEnterpriseConfiguration = true, IsEnterpriseConfiguration = true,
EnterpriseConfigurationPluginId = configPluginId, EnterpriseConfigurationPluginId = configPluginId,
}; };
@@ -294,6 +354,43 @@ public record ChatTemplate(
return toolIds; return toolIds;
} }
/// <remarks>
/// The empty GUID is a statement for a profile — start without one — but names no provider at
/// all, hence the switch. Anything which is no GUID cannot point at a provider or profile and is
/// dropped, so that the template still works, with the chat defaults in its place.
/// </remarks>
private static string? ParseOptionalId(int idx, LuaTable table, string fieldName, bool allowEmpty)
{
if (!table.TryGetValue(fieldName, out var idValue))
return null;
if (!idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id) || (!allowEmpty && id == Guid.Empty))
{
LOGGER.LogWarning("The {FieldName} of chat template {IdxChatTemplate} is not a valid ID and will be ignored.", fieldName, idx);
return null;
}
return id.ToString();
}
/// <remarks>
/// An empty name would mean "no workspace", which is what a template says by leaving the field
/// out. So it is dropped, with a warning, as is anything which is no text.
/// </remarks>
private static string? ParseWorkspaceName(int idx, LuaTable table)
{
if (!table.TryGetValue("WorkspaceName", out var workspaceNameValue))
return null;
if (!workspaceNameValue.TryRead<string>(out var workspaceName) || string.IsNullOrWhiteSpace(workspaceName))
{
LOGGER.LogWarning("The WorkspaceName of chat template {IdxChatTemplate} is not a valid name and will be ignored.", idx);
return null;
}
return workspaceName.Trim();
}
private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table) private static DataSourceOptions? ParseDataSourceOptions(int idx, LuaTable table)
{ {
if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead<LuaTable>(out var optionsTable)) if (!table.TryGetValue("DataSourceOptions", out var optionsValue) || !optionsValue.TryRead<LuaTable>(out var optionsTable))
@@ -449,22 +546,25 @@ public record ChatTemplate(
var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths); var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths);
// //
// Both of these may be absent entirely, because saying nothing about tools or data sources // These may be absent entirely, because saying nothing about the profile, the provider, the
// is a statement of its own. They therefore bring their own line break and indentation // workspace, the tools, or the data sources is a statement of its own. They therefore bring
// instead of sitting on a line of the template: // their own line break and indentation instead of sitting on a line of the template:
// //
var profileIdLua = BuildOptionalStringLua("ProfileId", this.ProfileId);
var providerIdLua = BuildOptionalStringLua("ProviderId", this.ProviderId);
var workspaceNameLua = BuildOptionalStringLua("WorkspaceName", this.WorkspaceName);
var toolIdsLua = this.BuildToolIdsLua(); var toolIdsLua = this.BuildToolIdsLua();
var dataSourceOptionsLua = this.BuildDataSourceOptionsLua(); var dataSourceOptionsLua = this.BuildDataSourceOptionsLua();
luaCode = $$""" luaCode = $$"""
{{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { {{this.BuildProviderAndProfileIdNote()}}{{this.BuildDataSourceIdNote()}}CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}", ["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}",
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}}, ["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}}, ["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}},
["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}}, ["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}},
["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}}, ["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}},
["FileAttachments"] = {{fileAttachmentsLua}}, ["FileAttachments"] = {{fileAttachmentsLua}},
["ExampleConversation"] = {{exampleConversationLua}},{{toolIdsLua}}{{dataSourceOptionsLua}} ["ExampleConversation"] = {{exampleConversationLua}},{{profileIdLua}}{{providerIdLua}}{{workspaceNameLua}}{{toolIdsLua}}{{dataSourceOptionsLua}}
} }
"""; """;
return true; return true;
@@ -574,6 +674,10 @@ public record ChatTemplate(
return true; return true;
} }
private static string BuildOptionalStringLua(string fieldName, string? value) => value is null
? string.Empty
: $"{Environment.NewLine} [\"{fieldName}\"] = {LuaTools.ToLuaStringLiteral(value)},";
/// <remarks> /// <remarks>
/// An empty set is written out as an empty table rather than being left out: the two say /// An empty set is written out as an empty table rather than being left out: the two say
/// different things, and dropping the line would turn "no tools at all" into "whatever the /// different things, and dropping the line would turn "no tools at all" into "whatever the
@@ -653,6 +757,26 @@ public record ChatTemplate(
"""; """;
} }
/// <remarks>
/// The same holds for the provider and the profile: a configuration plugin reaches only those
/// which it or another plugin defines, and the ones of this machine are mostly the user's own.
/// Starting without a profile names nothing, so it needs no note.
/// </remarks>
private string BuildProviderAndProfileIdNote()
{
var namesProfile = this.ProfileId is not null && !this.StartsWithoutProfile();
if (this.ProviderId is null && !namesProfile)
return string.Empty;
return """
-- The provider and profile IDs below are the ones of the machine this was exported from.
-- Please check them against your CONFIG["LLM_PROVIDERS"] and CONFIG["PROFILES"]: an ID
-- which resolves to nothing is ignored, and a chat with this template then starts with
-- the provider or profile it would start with anyway.
""";
}
private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths) private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths)
{ {
var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList(); var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList();
@@ -0,0 +1,16 @@
using AIStudio.Settings.DataModel;
namespace AIStudio.Settings;
/// <summary>
/// Whose data source options a chat started by a launcher begins with, decided by ChatTemplate.ChooseDataSourceOptions.
/// </summary>
/// <param name="Options">The options to start with, or null when neither side names any, which leaves the chat default in place.</param>
/// <param name="LauncherChoiceDropped">Whether the data sources the launcher names itself were dropped for the options of the chat template.</param>
public readonly record struct ChatTemplateDataSourceChoice(DataSourceOptions? Options, bool LauncherChoiceDropped)
{
/// <summary>
/// Neither side names any data sources, so the chat keeps its default options.
/// </summary>
public static readonly ChatTemplateDataSourceChoice NONE = new(null, false);
}
@@ -0,0 +1,14 @@
namespace AIStudio.Settings;
/// <summary>
/// Whose tools a chat started by a launcher begins with, decided by ChatTemplate.ChooseToolIds.
/// </summary>
/// <param name="ToolIds">The tools to start with, or null when neither side names any, which leaves the chat default in place.</param>
/// <param name="LauncherChoiceDropped">Whether the tools the launcher names itself were dropped for those of the chat template.</param>
public readonly record struct ChatTemplateToolChoice(IReadOnlyCollection<string>? ToolIds, bool LauncherChoiceDropped)
{
/// <summary>
/// Neither side names any tools, so the chat keeps its default.
/// </summary>
public static readonly ChatTemplateToolChoice NONE = new(null, false);
}
@@ -206,6 +206,11 @@ public sealed class Data
public DataSlideBuilder SlideBuilder { get; init; } = new(); public DataSlideBuilder SlideBuilder { get; init; } = new();
/// <summary>
/// Gets the managed Assistant Builder defaults.
/// </summary>
public DataAssistantBuilder AssistantBuilder { get; init; } = new(x => x.AssistantBuilder);
/// <summary> /// <summary>
/// Gets the managed Visual Briefing Assistant defaults. /// Gets the managed Visual Briefing Assistant defaults.
/// </summary> /// </summary>
@@ -0,0 +1,44 @@
using System.Linq.Expressions;
using AIStudio.Provider;
namespace AIStudio.Settings.DataModel;
/// <summary>
/// Stores the default options for the Assistant Builder.
/// </summary>
/// <param name="configSelection">The managed-configuration selector.</param>
public sealed class DataAssistantBuilder(Expression<Func<Data, DataAssistantBuilder>>? configSelection = null)
{
/// <summary>
/// Initializes an unmanaged Assistant Builder settings instance.
/// </summary>
public DataAssistantBuilder() : this(null)
{
}
/// <summary>
/// Gets or sets whether Assistant Builder defaults are preselected.
/// </summary>
public bool PreselectOptions { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectOptions, true);
/// <summary>
/// Gets or sets the provider used by default. An empty value uses the app default.
/// </summary>
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProvider, string.Empty);
/// <summary>
/// Gets or sets the default output language for generated assistants.
/// </summary>
public CommonLanguages PreselectedOutputLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedOutputLanguage, CommonLanguages.AS_IS);
/// <summary>
/// Gets or sets the default custom output language.
/// </summary>
public string PreselectedOtherOutputLanguage { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedOtherOutputLanguage, string.Empty);
/// <summary>
/// Gets or sets the minimum confidence a provider needs for building assistants.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
}
@@ -11,7 +11,7 @@ namespace AIStudio.Settings.DataModel;
/// <param name="configSelection">The managed-configuration selector.</param> /// <param name="configSelection">The managed-configuration selector.</param>
public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessing>>? configSelection = null) public sealed class DataBatchProcessing(Expression<Func<Data, DataBatchProcessing>>? configSelection = null)
{ {
public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm"; public const string DEFAULT_FILE_PATTERNS = "*.pdf;*.docx;*.pptx;*.xlsx;*.md;*.txt;*.jpg;*.jpeg;*.png;*.webp;*.gif;*.bmp;*.tif;*.tiff;*.mp3;*.wav;*.wave;*.aac;*.flac;*.ogg;*.opus;*.m4a;*.m4b;*.wma;*.alac;*.aif;*.aiff;*.caf;*.mp4;*.m4v;*.avi;*.mkv;*.mov;*.wmv;*.flv;*.webm";
public const int MIN_DELAY_SECONDS = 6; public const int MIN_DELAY_SECONDS = 6;
public const int MAX_DELAY_SECONDS = 300; public const int MAX_DELAY_SECONDS = 300;
public const int DEFAULT_MIN_DELAY_SECONDS = 6; public const int DEFAULT_MIN_DELAY_SECONDS = 6;
@@ -81,10 +81,17 @@ public static partial class ProviderExtensions
/// wants to know whether an image may be sent has to accept both, which is why the question is asked /// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must /// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it. /// never disagree about it.
///
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator
/// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks> /// </remarks>
/// <param name="provider">The configured provider.</param> /// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns> /// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider) => provider.GetModelProfile().HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT); public static bool SupportsImageInput(this Provider provider)
{
var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
}
/// <summary> /// <summary>
/// Checks whether this model can be used for chatting. /// Checks whether this model can be used for chatting.
@@ -877,6 +877,149 @@ public sealed class SettingsManager
return this.GetChatTemplateById(this.ConfigurationData.App.PreselectedChatTemplate); return this.GetChatTemplateById(this.ConfigurationData.App.PreselectedChatTemplate);
} }
/// <summary>
/// Returns the profile a chat template preselects for the chats started with it.
/// </summary>
/// <remarks>
/// Null whenever the template has nothing to say which could be used: it names no profile, it
/// does not allow profiles, or the profile it names is gone. The chat then keeps the profile it
/// would have without the template. Starting without a profile is an answer of its own, and it
/// comes back as Profile.NO_PROFILE.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <returns>The profile to start with, or null to keep the usual one.</returns>
public Profile? GetChatTemplateProfile(ChatTemplate chatTemplate)
{
if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null)
return null;
if (chatTemplate.StartsWithoutProfile())
return Profile.NO_PROFILE;
var profile = this.GetProfileById(chatTemplate.ProfileId);
return profile == Profile.NO_PROFILE ? null : profile;
}
/// <summary>
/// Returns the profile a chat started with the given chat template begins with.
/// </summary>
/// <remarks>
/// Null when the template says nothing about profiles, so the chat keeps the profile it has.
/// A template whose profile is gone starts its chats with the default profile of the chat
/// settings, which may be none — not with whatever profile the previous chat happened to use,
/// which would be a choice nobody made for this chat.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <returns>The profile to start with, or null to keep the current one.</returns>
public Profile? GetChatTemplateStartProfile(ChatTemplate chatTemplate)
{
if (!chatTemplate.AllowProfileUsage || chatTemplate.ProfileId is null)
return null;
return this.GetChatTemplateProfile(chatTemplate) ?? this.GetPreselectedProfile(Tools.Components.CHAT);
}
/// <summary>
/// Returns the provider a chat template preselects for the chats started with it.
/// </summary>
/// <remarks>
/// The template is no way around the confidence levels: a provider which may not be used for
/// chats comes back as Provider.NONE, just like one which is gone or one the
/// template does not name at all. The chat then keeps the provider it would have without the
/// template.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <returns>The provider to start with, or Provider.NONE to keep the usual one.</returns>
public Provider GetChatTemplateProvider(ChatTemplate chatTemplate) => this.GetChatTemplateProvider(chatTemplate.ProviderId);
/// <summary>
/// Returns the provider a chat template may preselect under the given ID.
/// </summary>
/// <remarks>
/// The one rule for the provider of a chat template: it exists and meets the confidence level
/// required for chats. The chat, assistant chat launchers, the chat template dialog, and the
/// import of a chat template all ask here, so that none of them accepts a provider another
/// one would leave out.
/// </remarks>
/// <param name="providerId">The ID the chat template names, or null when it names none.</param>
/// <returns>The provider, or Provider.NONE when the ID names no provider a chat may use.</returns>
public Provider GetChatTemplateProvider(string? providerId)
{
if (string.IsNullOrWhiteSpace(providerId))
return Provider.NONE;
var provider = this.GetProviderById(providerId);
return provider != Provider.NONE && this.IsProviderConfident(provider, Tools.Components.CHAT) ? provider : Provider.NONE;
}
/// <summary>
/// Returns the profile a chat begins with when it starts with the given chat template now.
/// </summary>
/// <remarks>
/// A template which names a profile decides, see GetChatTemplateStartProfile. One
/// which says nothing keeps the current profile, with one exception: when that profile is the one
/// the previous template preselected, the chat starts with the default profile of the chat
/// settings instead. Keeping it would carry the choice of another template over into this chat.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <param name="previousChatTemplate">The chat template the current profile was chosen with; the same one when the template does not change.</param>
/// <param name="currentProfile">The profile the chat has right now.</param>
/// <returns>The profile to start with.</returns>
public Profile ChooseChatTemplateProfile(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Profile currentProfile)
{
if (this.GetChatTemplateStartProfile(chatTemplate) is { } templateProfile)
return templateProfile;
if (this.GetChatTemplateStartProfile(previousChatTemplate) is { } previousTemplateProfile && previousTemplateProfile.Id == currentProfile.Id)
return this.GetPreselectedProfile(Tools.Components.CHAT);
return currentProfile;
}
/// <summary>
/// Returns the provider a chat begins with when it starts with the given chat template now.
/// </summary>
/// <remarks>
/// The same as for the profile, see ChooseChatTemplateProfile: a template which
/// names a usable provider decides, and the provider of the previous template is not carried
/// over. In its place comes the default provider of the chat settings. Without such a default,
/// the current provider stays, so that the chat does not lose its provider altogether.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <param name="previousChatTemplate">The chat template the current provider was chosen with; the same one when the template does not change.</param>
/// <param name="currentProvider">The provider the chat has right now.</param>
/// <returns>The provider to start with.</returns>
public Provider ChooseChatTemplateProvider(ChatTemplate chatTemplate, ChatTemplate previousChatTemplate, Provider currentProvider)
{
var templateProvider = this.GetChatTemplateProvider(chatTemplate);
if (templateProvider != Provider.NONE)
return templateProvider;
var previousTemplateProvider = this.GetChatTemplateProvider(previousChatTemplate);
if (previousTemplateProvider == Provider.NONE || previousTemplateProvider.Id != currentProvider.Id)
return currentProvider;
var defaultProvider = this.GetPreselectedProvider(Tools.Components.CHAT);
return defaultProvider == Provider.NONE ? currentProvider : defaultProvider;
}
/// <summary>
/// Returns the name of the workspace a chat template keeps the chats started with it in.
/// </summary>
/// <remarks>
/// Null when the template names none, and also while workspaces are switched off: a chat then
/// belongs to no workspace, whatever its template says.
/// </remarks>
/// <param name="chatTemplate">The chat template of the chat about to start.</param>
/// <returns>The trimmed name of the workspace, or null to keep the chat where it would go anyway.</returns>
public string? GetChatTemplateWorkspaceName(ChatTemplate chatTemplate)
{
if (string.IsNullOrWhiteSpace(chatTemplate.WorkspaceName) || this.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.DISABLE_WORKSPACES)
return null;
return chatTemplate.WorkspaceName.Trim();
}
public Profile GetProfileById(string? profileId) public Profile GetProfileById(string? profileId)
{ {
if (string.IsNullOrWhiteSpace(profileId)) if (string.IsNullOrWhiteSpace(profileId))
@@ -152,6 +152,7 @@ public static class ComponentsExtensions
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default, Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default, Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence, Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
// The minimum confidence for the Document Analysis Assistant is set per policy. // The minimum confidence for the Document Analysis Assistant is set per policy.
@@ -184,6 +185,7 @@ public static class ComponentsExtensions
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE, Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE, Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE, Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider), Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
// The Document Analysis Assistant does not have a preselected provider at the component level. // The Document Analysis Assistant does not have a preselected provider at the component level.
@@ -0,0 +1,115 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Tools;
/// <summary>
/// Opens the settings of a data source, and stores what the user changed in them.
/// </summary>
/// <remarks>
/// Kept here rather than in the places which offer it -- the data source table and the background
/// embeddings page, which leads to it when the file or the folder of a data source cannot be
/// reached -- so that saving a data source works the same way everywhere: the settings are stored,
/// and the data source is indexed right after. Mailboxes are kept in a list of their own and have
/// MailboxEditing instead.
/// </remarks>
public static class DataSourceEditing
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceEditing).Namespace, nameof(DataSourceEditing));
/// <summary>
/// Whether the user may edit a data source.
/// </summary>
/// <param name="settingsManager">The settings, which hold the data source.</param>
/// <param name="dataSourceId">The id of the data source.</param>
/// <returns>False when the data source is gone or managed by the organization.</returns>
public static bool CanEdit(SettingsManager settingsManager, string dataSourceId) =>
settingsManager.ConfigurationData.DataSources.Any(dataSource => dataSource.Id == dataSourceId && !dataSource.IsEnterpriseConfiguration);
/// <summary>
/// Lets the user edit a data source, and stores and indexes it when they save.
/// </summary>
/// <remarks>
/// The caller tells the rest of the app that the configuration changed, as the sender of that message.
/// </remarks>
/// <param name="dialogService">The dialog service to show the settings with.</param>
/// <param name="settingsManager">The settings, which hold the data source.</param>
/// <param name="embeddingService">The service which indexes the data source.</param>
/// <param name="dataSourceId">The id of the data source.</param>
/// <returns>True when the user saved the data source. False when they cancelled, or the data source is gone or managed by the organization.</returns>
public static async Task<bool> EditAsync(IDialogService dialogService, SettingsManager settingsManager, DataSourceEmbeddingService embeddingService, string dataSourceId)
{
var dataSources = settingsManager.ConfigurationData.DataSources;
var storedDataSource = dataSources.Find(dataSource => dataSource.Id == dataSourceId);
if (storedDataSource is null || storedDataSource.IsEnterpriseConfiguration)
return false;
IDataSource? editedDataSource = null;
switch (storedDataSource)
{
case DataSourceLocalFile localFile:
var localFileDialogParameters = new DialogParameters<DataSourceLocalFileDialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, localFile },
};
var localFileDialogReference = await dialogService.ShowAsync<DataSourceLocalFileDialog>(TB("Edit Local File Data Source"), localFileDialogParameters, DialogOptions.FULLSCREEN);
var localFileDialogResult = await localFileDialogReference.Result;
if (localFileDialogResult is null || localFileDialogResult.Canceled)
return false;
editedDataSource = (DataSourceLocalFile)localFileDialogResult.Data!;
break;
case DataSourceLocalDirectory localDirectory:
var localDirectoryDialogParameters = new DialogParameters<DataSourceLocalDirectoryDialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, localDirectory },
};
var localDirectoryDialogReference = await dialogService.ShowAsync<DataSourceLocalDirectoryDialog>(TB("Edit Local Directory Data Source"), localDirectoryDialogParameters, DialogOptions.FULLSCREEN);
var localDirectoryDialogResult = await localDirectoryDialogReference.Result;
if (localDirectoryDialogResult is null || localDirectoryDialogResult.Canceled)
return false;
editedDataSource = (DataSourceLocalDirectory)localDirectoryDialogResult.Data!;
break;
case DataSourceERI_V1 eriDataSource:
var eriDialogParameters = new DialogParameters<DataSourceERI_V1Dialog>
{
{ x => x.IsEditing, true },
{ x => x.DataSource, eriDataSource },
};
var eriDialogReference = await dialogService.ShowAsync<DataSourceERI_V1Dialog>(TB("Edit ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
if (eriDialogResult is null || eriDialogResult.Canceled)
return false;
editedDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
break;
}
if (editedDataSource is null)
return false;
// Found anew, since the list may have changed while the dialog was open:
var position = dataSources.IndexOf(storedDataSource);
if (position < 0)
return false;
dataSources[position] = editedDataSource;
await settingsManager.StoreSettings();
await embeddingService.QueueDataSourceAsync(editedDataSource);
return true;
}
}
@@ -2,6 +2,7 @@ using System.Text;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
@@ -19,6 +20,10 @@ namespace AIStudio.Tools;
/// Nothing is asked when nothing is lost. A data source only reaches the question when the edit /// Nothing is asked when nothing is lost. A data source only reaches the question when the edit
/// really changes its embedding signature and when the index already holds something for it, so /// really changes its embedding signature and when the index already holds something for it, so
/// renaming an embedding provider or editing a data source nobody has indexed yet stays silent. /// renaming an embedding provider or editing a data source nobody has indexed yet stays silent.
/// A new location of a local file or folder has a question of its own, under the same condition of
/// a stored index: the signature stays, but every file which is not found unchanged at the new
/// location is prepared again. When the signature changes as well, only its question is asked,
/// since everything is prepared again anyway.
/// </remarks> /// </remarks>
public static class DataSourceReindexWarning public static class DataSourceReindexWarning
{ {
@@ -84,13 +89,62 @@ public static class DataSourceReindexWarning
DataSourceEmbeddingProviders.TryResolve(settingsManager, before, out var resolvedBeforeProvider); DataSourceEmbeddingProviders.TryResolve(settingsManager, before, out var resolvedBeforeProvider);
var beforeProvider = resolvedBeforeProvider ?? EmbeddingProvider.NONE; var beforeProvider = resolvedBeforeProvider ?? EmbeddingProvider.NONE;
if (!EmbeddingChangeImpact.AffectsStoredIndex(before, beforeProvider, after, afterProvider)) // Everything is prepared again, wherever the files are, so this question covers a new location as well:
return true; if (EmbeddingChangeImpact.AffectsStoredIndex(before, beforeProvider, after, afterProvider))
{
var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync([after], token); var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync([after], token);
return await ConfirmAsync(dialogService, affected, !afterProvider.IsSelfHosted); return await ConfirmAsync(dialogService, affected, !afterProvider.IsSelfHosted);
} }
if (!HasLocationChanged(before, after))
return true;
var indexed = await embeddingService.GetDataSourcesWithStoredIndexAsync([after], token);
return indexed.Count == 0 || await ConfirmNewLocationAsync(dialogService, !afterProvider.IsSelfHosted);
}
private static bool HasLocationChanged(IIndexedDataSource before, IIndexedDataSource after) => (before, after) switch
{
(DataSourceLocalDirectory oldFolder, DataSourceLocalDirectory newFolder) => !string.Equals(oldFolder.Path, newFolder.Path, StringComparison.Ordinal),
(DataSourceLocalFile oldFile, DataSourceLocalFile newFile) => !string.Equals(oldFile.FilePath, newFile.FilePath, StringComparison.Ordinal),
_ => false,
};
/// <summary>
/// Asks before a local file or folder with a stored index is saved with a new location.
/// </summary>
/// <remarks>
/// The conditions for keeping a file are the ones FileSourceIndexer.FindRelocatedFile checks.
/// </remarks>
/// <param name="dialogService">The dialog service to ask with.</param>
/// <param name="usesCloudEmbedding">Whether preparing a file again costs money.</param>
/// <returns>True when the edit may be saved.</returns>
private static async Task<bool> ConfirmNewLocationAsync(IDialogService dialogService, bool usesCloudEmbedding)
{
var body = new StringBuilder();
body.AppendLine(TB("AI Studio keeps what it has already prepared for every file it finds unchanged at the new location, that is, of the same type and with the same size and modification time. In a folder, such a file also has to keep its name and the subfolder it is in."));
body.AppendLine();
body.AppendLine(TB("Every other file goes to your embedding provider once more, which takes a while with many files. Files missing at the new location are removed from the index, while your files on disk stay as they are."));
if (usesCloudEmbedding)
{
body.AppendLine();
body.AppendLine(TB("Your embedding provider runs in the cloud, so preparing files again costs money."));
}
body.AppendLine();
body.AppendLine(TB("Do you want to apply this change anyway?"));
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.MarkdownBody, body.ToString() },
};
var dialogReference = await dialogService.ShowAsync<ConfirmDialog>(TB("New Location for This Data Source"), dialogParameters, Dialogs.DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
return dialogResult is not null && !dialogResult.Canceled;
}
/// <summary> /// <summary>
/// Names the data sources which would lose their embedding provider, for the deletion question. /// Names the data sources which would lose their embedding provider, for the deletion question.
/// </summary> /// </summary>
@@ -29,6 +29,11 @@ public abstract partial class IndexStoreClient(string name, string path) : Datab
public abstract Task UpsertFileAsync(string dataSourceId, EmbeddingStateFile file, CancellationToken token); public abstract Task UpsertFileAsync(string dataSourceId, EmbeddingStateFile file, CancellationToken token);
/// <summary>
/// Updates the location and fingerprint of a document, preserving its chunks and their ids.
/// </summary>
public abstract Task RelocateFileAsync(string dataSourceId, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token);
public abstract Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token); public abstract Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token);
public abstract Task UpsertPermanentFailureAsync(string dataSourceId, PermanentIndexingFailure failure, CancellationToken token); public abstract Task UpsertPermanentFailureAsync(string dataSourceId, PermanentIndexingFailure failure, CancellationToken token);
@@ -52,6 +52,9 @@ public sealed partial class NoIndexStoreClient(string name, string? unavailableR
public override Task UpsertFileAsync(string dataSourceId, EmbeddingStateFile file, CancellationToken token) => Task.CompletedTask; public override Task UpsertFileAsync(string dataSourceId, EmbeddingStateFile file, CancellationToken token) => Task.CompletedTask;
public override Task RelocateFileAsync(string dataSourceId, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token) =>
Task.FromException(new InvalidOperationException(unavailableReason ?? "The index of the data sources is not available."));
public override Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token) => Task.CompletedTask; public override Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token) => Task.CompletedTask;
public override Task UpsertPermanentFailureAsync(string dataSourceId, PermanentIndexingFailure failure, CancellationToken token) => Task.CompletedTask; public override Task UpsertPermanentFailureAsync(string dataSourceId, PermanentIndexingFailure failure, CancellationToken token) => Task.CompletedTask;
@@ -149,7 +149,9 @@ public sealed partial class SqliteIndexStoreClientImplementation(string name, st
file.LastWriteUtc, file.LastWriteUtc,
file.EmbeddedAtUtc, file.EmbeddedAtUtc,
file.ChunkCount, file.ChunkCount,
file.TranscriptKey); file.TranscriptKey,
file.RelativePath,
file.ParentFileId);
} }
var permanentFailures = await context.PermanentIndexingFailures var permanentFailures = await context.PermanentIndexingFailures
@@ -233,6 +235,18 @@ public sealed partial class SqliteIndexStoreClientImplementation(string name, st
await context.SaveChangesAsync(token); await context.SaveChangesAsync(token);
} }
public override async Task RelocateFileAsync(string dataSourceId, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token)
{
await using var context = this.CreateContext();
var file = await context.EmbeddedFiles.SingleAsync(file => file.DataSourceId == dataSourceId && file.ParentFileId == parentFileId, token);
// Keep the parent and chunk ids: moving a document does not change its stored vectors.
file.AbsolutePath = absolutePath;
file.RelativePath = relativePath;
file.FileName = Path.GetFileName(absolutePath);
file.Fingerprint = fingerprint;
await context.SaveChangesAsync(token);
}
public override async Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token) public override async Task DeleteFileAsync(string dataSourceId, string filePath, CancellationToken token)
{ {
await using var context = this.CreateContext(); await using var context = this.CreateContext();
@@ -37,7 +37,10 @@ public sealed class NoVectorStoreClient(string name, string? unavailableReason,
public override Task<IReadOnlyList<VectorSearchResult>> SearchEmbeddingAsync(string storeName, IReadOnlyList<float> vector, int maxMatches, VectorSearchFilter filter, CancellationToken token) => public override Task<IReadOnlyList<VectorSearchResult>> SearchEmbeddingAsync(string storeName, IReadOnlyList<float> vector, int maxMatches, VectorSearchFilter filter, CancellationToken token) =>
Task.FromException<IReadOnlyList<VectorSearchResult>>(this.CreateUnavailableException()); Task.FromException<IReadOnlyList<VectorSearchResult>>(this.CreateUnavailableException());
public override Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token) => public override Task DeleteEmbeddingByFile(string storeName, string filePath, string parentFileId, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException());
public override Task RelocateEmbeddingFileAsync(string storeName, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException()); Task.FromException(this.CreateUnavailableException());
public override Task OptimizeVectorStore(string storeName, CancellationToken token) => public override Task OptimizeVectorStore(string storeName, CancellationToken token) =>
@@ -27,6 +27,7 @@ public sealed class QdrantEdgeClientImplementation(
private const string INSERT_PATH = "/system/qdrant-edge/insert"; private const string INSERT_PATH = "/system/qdrant-edge/insert";
private const string SEARCH_PATH = "/system/qdrant-edge/search"; private const string SEARCH_PATH = "/system/qdrant-edge/search";
private const string DELETE_FILE_PATH = "/system/qdrant-edge/delete-file"; private const string DELETE_FILE_PATH = "/system/qdrant-edge/delete-file";
private const string RELOCATE_FILE_PATH = "/system/qdrant-edge/relocate-file";
private const string OPTIMIZE_PATH = "/system/qdrant-edge/optimize"; private const string OPTIMIZE_PATH = "/system/qdrant-edge/optimize";
private const string DELETE_STORE_PATH = "/system/qdrant-edge/delete-store"; private const string DELETE_STORE_PATH = "/system/qdrant-edge/delete-store";
private const string PREPARE_BACKUP_PATH = "/system/qdrant-edge/backup/prepare"; private const string PREPARE_BACKUP_PATH = "/system/qdrant-edge/backup/prepare";
@@ -156,8 +157,11 @@ public sealed class QdrantEdgeClientImplementation(
token) ?? []; token) ?? [];
} }
public override Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token) => public override Task DeleteEmbeddingByFile(string storeName, string filePath, string parentFileId, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_FILE_PATH, new DeleteEmbeddingByFileRequest(storeName, filePath), token); rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_FILE_PATH, new DeleteEmbeddingByFileRequest(storeName, filePath, parentFileId), token);
public override Task RelocateEmbeddingFileAsync(string storeName, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, RELOCATE_FILE_PATH, new RelocateEmbeddingFileRequest(storeName, parentFileId, absolutePath, Path.GetFileName(absolutePath), relativePath, fingerprint), token);
public override Task OptimizeVectorStore(string storeName, CancellationToken token) => public override Task OptimizeVectorStore(string storeName, CancellationToken token) =>
rustService.ExecuteDatabaseOperation(DATABASE_NAME, OPTIMIZE_PATH, new OptimizeVectorStoreRequest(storeName), token); rustService.ExecuteDatabaseOperation(DATABASE_NAME, OPTIMIZE_PATH, new OptimizeVectorStoreRequest(storeName), token);
@@ -194,7 +198,9 @@ public sealed class QdrantEdgeClientImplementation(
/// <param name="PointIds">The only points the search may return, or null for the whole store.</param> /// <param name="PointIds">The only points the search may return, or null for the whole store.</param>
private sealed record SearchEmbeddingRequest(string StoreName, IReadOnlyList<float> Vector, int MaxMatches, IReadOnlyList<string>? PointIds); private sealed record SearchEmbeddingRequest(string StoreName, IReadOnlyList<float> Vector, int MaxMatches, IReadOnlyList<string>? PointIds);
private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath); private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath, string ParentFileId);
private sealed record RelocateEmbeddingFileRequest(string StoreName, string ParentFileId, string AbsolutePath, string FileName, string RelativePath, string Fingerprint);
private sealed record OptimizeVectorStoreRequest(string StoreName); private sealed record OptimizeVectorStoreRequest(string StoreName);
@@ -27,7 +27,12 @@ public abstract class VectorStoreClient(string name, string path): DatabaseClien
/// <returns>The matches, best first, all of them inside the filter.</returns> /// <returns>The matches, best first, all of them inside the filter.</returns>
public abstract Task<IReadOnlyList<VectorSearchResult>> SearchEmbeddingAsync(string storeName, IReadOnlyList<float> vector, int maxMatches, VectorSearchFilter filter, CancellationToken token); public abstract Task<IReadOnlyList<VectorSearchResult>> SearchEmbeddingAsync(string storeName, IReadOnlyList<float> vector, int maxMatches, VectorSearchFilter filter, CancellationToken token);
public abstract Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token); public abstract Task DeleteEmbeddingByFile(string storeName, string filePath, string parentFileId, CancellationToken token);
/// <summary>
/// Updates the location of a document's points without changing their ids or vectors.
/// </summary>
public abstract Task RelocateEmbeddingFileAsync(string storeName, string parentFileId, string absolutePath, string relativePath, string fingerprint, CancellationToken token);
public abstract Task OptimizeVectorStore(string storeName, CancellationToken token); public abstract Task OptimizeVectorStore(string storeName, CancellationToken token);
@@ -43,7 +43,7 @@ public static class MailboxEditing
{ {
{ x => x.IsEditing, true }, { x => x.IsEditing, true },
{ x => x.DataSource, storedMailbox }, { x => x.DataSource, storedMailbox },
{ x => x.LockSource, await embeddingService.ShouldLockDataSourceOriginAsync(storedMailbox.Id) }, { x => x.LockSource, await embeddingService.ShouldLockMailboxOriginAsync(storedMailbox.Id) },
}; };
var dialogReference = await dialogService.ShowAsync<DataSourceMailboxDialog>(TB("Edit Mailbox"), dialogParameters, DialogOptions.FULLSCREEN); var dialogReference = await dialogService.ShowAsync<DataSourceMailboxDialog>(TB("Edit Mailbox"), dialogParameters, DialogOptions.FULLSCREEN);
+27 -1
View File
@@ -1,7 +1,7 @@
namespace AIStudio.Tools; namespace AIStudio.Tools;
/// <summary> /// <summary>
/// Shared rules for comparing file system paths. /// Shared rules for comparing and resolving file system paths.
/// </summary> /// </summary>
public static class PathTools public static class PathTools
{ {
@@ -42,4 +42,30 @@ public static class PathTools
return false; return false;
} }
} }
/// <summary>
/// Finds the place where a file dialog opens for a path which may no longer exist.
/// </summary>
/// <remarks>
/// Given a file or a folder which was moved or renamed, the native dialog cannot open its place
/// and falls back to wherever it was used last. The nearest folder which still exists is the
/// better start: the file or the folder was moved from there, so its new place is often close by.
/// A relative path, or a path of another operating system, e.g. from a backup, has no such folder.
/// </remarks>
/// <param name="path">The file or the folder the dialog should open at.</param>
/// <returns>The path itself when it exists, else its nearest parent folder which exists, or null when there is none.</returns>
public static string? FindNearestExistingPath(string path)
{
if (string.IsNullOrWhiteSpace(path) || !Path.IsPathFullyQualified(path))
return null;
if (File.Exists(path) || Directory.Exists(path))
return path;
for (var directory = Path.GetDirectoryName(path); !string.IsNullOrEmpty(directory); directory = Path.GetDirectoryName(directory))
if (Directory.Exists(directory))
return directory;
return null;
}
} }
@@ -148,6 +148,20 @@ public static class ConfigurationSnippetImport
if (missingChatToolIds.Count > 0) if (missingChatToolIds.Count > 0)
hints.Add(string.Format(TB("The chat template preselects tools which are not available on this computer: {0}."), string.Join(", ", missingChatToolIds))); hints.Add(string.Format(TB("The chat template preselects tools which are not available on this computer: {0}."), string.Join(", ", missingChatToolIds)));
if (template.ProviderId is not null)
{
var templateProvider = settingsManager.GetProviderById(template.ProviderId);
if (templateProvider == Settings.Provider.NONE)
hints.Add(string.Format(TB("The chat template preselects a provider which does not exist on this computer: {0}. Chats started with it use your usual provider instead."), template.ProviderId));
else if (settingsManager.GetChatTemplateProvider(template.ProviderId) == Settings.Provider.NONE)
hints.Add(string.Format(TB("The chat template preselects the provider '{0}', which does not meet the confidence level required for chats. Chats started with it use your usual provider instead."), templateProvider.InstanceName));
}
if (template.ProfileId is not null && !template.AllowProfileUsage)
hints.Add(TB("The chat template preselects a profile but does not allow profiles. Chats started with it ignore that profile."));
else if (template.ProfileId is not null && !template.StartsWithoutProfile() && settingsManager.GetProfileById(template.ProfileId) == Profile.NO_PROFILE)
hints.Add(string.Format(TB("The chat template preselects a profile which does not exist on this computer: {0}. Chats started with it use your usual profile instead."), template.ProfileId));
return result with { ConfigurationObject = template with { ToolIds = templateToolIds }, Hints = hints }; return result with { ConfigurationObject = template with { ToolIds = templateToolIds }, Hints = hints };
case DataDocumentAnalysisPolicy policy: case DataDocumentAnalysisPolicy policy:
@@ -474,6 +474,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.OptimizeImages, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.OptimizeImages, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.VisualBriefing, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
// Config: Assistant Builder defaults?
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectedOutputLanguage, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.PreselectedOtherOutputLanguage, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.AssistantBuilder, x => x.MinimumProviderConfidence, this.Id, settingsTable, dryRun);
// Config: transcription provider? // Config: transcription provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UseTranscriptionProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@@ -1,6 +1,5 @@
using System.Text; using System.Text;
using AIStudio.Chat;
using AIStudio.Tools.Security; using AIStudio.Tools.Security;
namespace AIStudio.Tools.RAG; namespace AIStudio.Tools.RAG;
+13 -1
View File
@@ -98,7 +98,19 @@ public static class FileTypes
// Media hierarchy // Media hierarchy
public static readonly FileTypeFilter IMAGE = FileTypeFilter.Leaf(TB("Image"), public static readonly FileTypeFilter IMAGE = FileTypeFilter.Leaf(TB("Image"),
"jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "webp", "heic", "avif"); "jpg", "jpeg", "png", "gif", "bmp", "tif", "tiff", "svg", "webp", "heic", "avif");
/// <summary>
/// The image formats AI Studio can prepare for a model.
/// </summary>
/// <remarks>
/// These are the formats the runtime decodes before it sends an image to a model as JPEG or
/// PNG, see prepare_image_for_model in runtime/src/image.rs. Other images, such as HEIC, AVIF,
/// or SVG, cannot go to a model at all. The default file patterns of the batch processing
/// assistant list the same formats, see DataBatchProcessing.DEFAULT_FILE_PATTERNS.
/// </remarks>
public static readonly FileTypeFilter MODEL_IMAGE = FileTypeFilter.Leaf(TB("Image for AI models"),
"jpg", "jpeg", "png", "webp", "gif", "bmp", "tif", "tiff");
/// <summary> /// <summary>
/// Gets the prototype visual-asset image formats. /// Gets the prototype visual-asset image formats.
@@ -0,0 +1,63 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Why the Rust runtime could not prepare an image for an AI model.
/// </summary>
/// <remarks>
/// UNKNOWN comes first on purpose. The converter for the enums of the runtime falls back to the
/// default value for a code it does not know, e.g., from a newer runtime, and such a code must
/// never read as a prepared image.
/// </remarks>
public enum ModelImageErrorCode
{
/// <summary>
/// The runtime answered with a code this version does not know, or it could not be asked at all.
/// </summary>
UNKNOWN,
/// <summary>
/// No failure happened: the image was prepared.
/// </summary>
NONE,
//
// Codes reported by the Rust runtime:
//
FILE_NOT_FOUND,
/// <summary>
/// The file exists, but could not be opened, e.g., for lack of permission.
/// </summary>
FILE_NOT_READABLE,
/// <summary>
/// AI Studio cannot decode this image format, e.g., HEIC, AVIF, or SVG.
/// </summary>
UNSUPPORTED_FORMAT,
/// <summary>
/// The content of the file does not match its file extension.
/// </summary>
CONTENT_MISMATCH,
/// <summary>
/// The file is damaged or uses a variant of its format which cannot be decoded.
/// </summary>
NOT_DECODABLE,
/// <summary>
/// Decoding the image would need more memory than the runtime allows.
/// </summary>
TOO_MANY_PIXELS,
/// <summary>
/// Not even the smallest size and lowest quality the runtime accepts fit into the byte budget.
/// </summary>
BUDGET_NOT_REACHABLE,
/// <summary>
/// Any failure in the runtime which does not carry a code of its own.
/// </summary>
INTERNAL,
}
@@ -0,0 +1,36 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Rust;
/// <summary>
/// Translates why an image could not be prepared for an AI model into user-facing text.
/// </summary>
/// <remarks>
/// The runtime writes the technical details into the log. The texts here name what the user can
/// act on, such as converting the image into another format.
/// </remarks>
public static class ModelImageErrorCodeExtensions
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelImageErrorCodeExtensions).Namespace, nameof(ModelImageErrorCodeExtensions));
/// <summary>
/// Gets the localized message which explains why an image was not sent to the AI.
/// </summary>
/// <param name="code">Why the image could not be prepared.</param>
/// <param name="fileName">The name of the image file, as shown to the user.</param>
/// <returns>The localized message.</returns>
public static string ToUserMessage(this ModelImageErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName);
private static string ToUserMessageFormat(ModelImageErrorCode code) => code switch
{
ModelImageErrorCode.FILE_NOT_FOUND => TB("The image '{0}' does not exist anymore and was not sent."),
ModelImageErrorCode.FILE_NOT_READABLE => TB("The image '{0}' could not be opened and was not sent. When the image is stored on a network drive, the drive might be unavailable."),
ModelImageErrorCode.UNSUPPORTED_FORMAT => TB("The image '{0}' uses a format which AI Studio cannot give to an AI model, so it was not sent. Please convert the image to JPG or PNG."),
ModelImageErrorCode.CONTENT_MISMATCH => TB("The image '{0}' is not what its file extension claims, so it was not sent. Please correct its file extension."),
ModelImageErrorCode.NOT_DECODABLE => TB("The image '{0}' could not be read and was not sent. It might be damaged or transferred incompletely."),
ModelImageErrorCode.TOO_MANY_PIXELS => TB("The image '{0}' has too many pixels for AI Studio to prepare it, so it was not sent. Please scale the image down first."),
ModelImageErrorCode.BUDGET_NOT_REACHABLE => TB("The image '{0}' could not be made small enough for the selected provider, so it was not sent."),
_ => TB("AI Studio was not able to prepare the image '{0}' for the AI, so it was not sent."),
};
}
@@ -0,0 +1,39 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// An image as the Rust runtime prepared it for an AI model, or why it could not.
/// </summary>
/// <remarks>
/// The base64 and its MIME type travel side by side, so a provider which wants a data URL builds
/// one and a provider which wants them apart, such as Anthropic, never parses one.
/// </remarks>
/// <param name="ErrorCode">NONE when the image was prepared; then the other values describe it.</param>
/// <param name="Base64">The encoded image as base64, without a data URI prefix.</param>
/// <param name="MimeType">The MIME type of the encoded image: image/jpeg or image/png.</param>
/// <param name="Width">The width of the prepared image in pixels.</param>
/// <param name="Height">The height of the prepared image in pixels.</param>
/// <param name="WasResized">Whether the runtime scaled the image down.</param>
public sealed record PreparedModelImage(ModelImageErrorCode ErrorCode, string Base64, string MimeType, uint Width, uint Height, bool WasResized)
{
/// <summary>
/// Whether the image can go to the model.
/// </summary>
/// <remarks>
/// The image itself has to be there as well: a success without one would become an empty image
/// in the request, which providers refuse along with the whole request.
/// </remarks>
public bool IsPrepared => this.ErrorCode is ModelImageErrorCode.NONE && !string.IsNullOrEmpty(this.Base64);
/// <summary>
/// The image as a data URL, as the OpenAI-compatible APIs take it.
/// </summary>
/// <returns>The data URL.</returns>
public string ToDataUrl() => $"data:{this.MimeType};base64,{this.Base64}";
/// <summary>
/// An image which could not be prepared.
/// </summary>
/// <param name="errorCode">Why it could not be prepared.</param>
/// <returns>The failed image.</returns>
public static PreparedModelImage Failed(ModelImageErrorCode errorCode) => new(errorCode, string.Empty, string.Empty, 0, 0, false);
}
@@ -0,0 +1,10 @@
using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services;
/// <summary>
/// The data source options a chat started by an assistant chat launcher begins with.
/// </summary>
/// <param name="Options">The checked options of the chat, or null when the chosen data sources cannot be used.</param>
/// <param name="ErrorMessage">Why no chat can be created with the chosen data sources, or an empty string when it can.</param>
internal readonly record struct ChatLauncherDataSourcesResult(DataSourceOptions? Options, string ErrorMessage);
@@ -0,0 +1,10 @@
using AIStudio.Settings;
namespace AIStudio.Tools.Services;
/// <summary>
/// The profile a chat started by an assistant chat launcher begins with.
/// </summary>
/// <param name="Profile">The profile of the chat, or null when the profile the launcher names does not exist.</param>
/// <param name="ErrorMessage">Why no profile could be chosen, or an empty string when one was.</param>
internal readonly record struct ChatLauncherProfileResult(Profile? Profile, string ErrorMessage);
@@ -0,0 +1,11 @@
using ProviderSettings = AIStudio.Settings.Provider;
namespace AIStudio.Tools.Services;
/// <summary>
/// The provider a chat started by an assistant chat launcher begins with.
/// </summary>
/// <param name="Provider">The provider of the chat, or NONE when there is none to start with.</param>
/// <param name="IsExplicit">Whether the chat template or the launcher named this provider, as opposed to the chat defaults.</param>
/// <param name="ErrorMessage">Why the named provider cannot be used, or an empty string when it can.</param>
internal readonly record struct ChatLauncherProviderResult(ProviderSettings Provider, bool IsExplicit, string ErrorMessage);
@@ -0,0 +1,10 @@
using AIStudio.Settings;
namespace AIStudio.Tools.Services;
/// <summary>
/// The chat template a chat started by an assistant chat launcher begins with.
/// </summary>
/// <param name="ChatTemplate">The chat template of the chat, or null when the template the launcher names does not exist.</param>
/// <param name="ErrorMessage">Why no chat template could be chosen, or an empty string when one was.</param>
internal readonly record struct ChatLauncherTemplateResult(ChatTemplate? ChatTemplate, string ErrorMessage);
@@ -24,4 +24,9 @@ public enum DataSourceAttention
/// The organization allows only its own mail servers, and the mailbox is on another one. AI Studio does not connect to it until the organization allows the server. /// The organization allows only its own mail servers, and the mailbox is on another one. AI Studio does not connect to it until the organization allows the server.
/// </summary> /// </summary>
SERVER_NOT_ALLOWED, SERVER_NOT_ALLOWED,
/// <summary>
/// The file or the folder of a local data source cannot be reached, cannot be opened, or appears empty. AI Studio keeps the index until the file or the folder is back, the user selects its new location, or removes the data source.
/// </summary>
LOCATION_UNREACHABLE,
} }
@@ -1,7 +1,27 @@
namespace AIStudio.Tools.Services; namespace AIStudio.Tools.Services;
/// <summary>
/// What the index store holds for one data source, read at the start of a run.
/// </summary>
public sealed class DataSourceEmbeddingManifest public sealed class DataSourceEmbeddingManifest
{ {
/// <summary>
/// How the index compares the paths of the documents it stores, on every OS.
/// </summary>
/// <remarks>
/// Ignores case, unlike PathTools.COMPARER, which follows the file system. The index store keeps
/// the paths with the NOCASE collation and allows each path of a data source once, so two files
/// which differ in their case alone share one row there. Whatever the indexer keys or compares by
/// these paths has to agree with the store; telling the two files apart would let them replace
/// each other's row on every run.
/// </remarks>
public static readonly StringComparer PATH_COMPARER = StringComparer.OrdinalIgnoreCase;
/// <summary>
/// The comparison matching PATH_COMPARER, e.g., for prefixes.
/// </summary>
public const StringComparison PATH_COMPARISON = StringComparison.OrdinalIgnoreCase;
public string EmbeddingProviderId { get; set; } = string.Empty; public string EmbeddingProviderId { get; set; } = string.Empty;
public string EmbeddingSignature { get; set; } = string.Empty; public string EmbeddingSignature { get; set; } = string.Empty;
@@ -10,7 +30,7 @@ public sealed class DataSourceEmbeddingManifest
public int VectorSize { get; set; } public int VectorSize { get; set; }
public Dictionary<string, EmbeddedFileRecord> Files { get; init; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, EmbeddedFileRecord> Files { get; init; } = new(PATH_COMPARER);
/// <summary> /// <summary>
/// The files whose indexing began but never came to an end, by their absolute path. /// The files whose indexing began but never came to an end, by their absolute path.
@@ -21,7 +41,7 @@ public sealed class DataSourceEmbeddingManifest
/// meanwhile. Indexing such a file again replaces both. A file which is gone, or which the data /// meanwhile. Indexing such a file again replaces both. A file which is gone, or which the data
/// source no longer selects, is never indexed again, so the indexer has to remove them itself. /// source no longer selects, is never indexed again, so the indexer has to remove them itself.
/// </remarks> /// </remarks>
public HashSet<string> UnfinishedFiles { get; init; } = new(StringComparer.OrdinalIgnoreCase); public HashSet<string> UnfinishedFiles { get; init; } = new(PATH_COMPARER);
/// <summary> /// <summary>
/// The files whose indexing failed for a reason which lies in the file itself, keyed by their /// The files whose indexing failed for a reason which lies in the file itself, keyed by their
@@ -32,5 +52,5 @@ public sealed class DataSourceEmbeddingManifest
/// a folder holding hundreds of scanned documents without a text layer would be read again on /// a folder holding hundreds of scanned documents without a text layer would be read again on
/// every single run, with the outcome known in advance. /// every single run, with the outcome known in advance.
/// </remarks> /// </remarks>
public Dictionary<string, PermanentIndexingFailureRecord> PermanentFailures { get; init; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, PermanentIndexingFailureRecord> PermanentFailures { get; init; } = new(PATH_COMPARER);
} }
@@ -158,14 +158,18 @@ public sealed partial class DataSourceEmbeddingService
/// files which no longer belong to the data source, and the ones which now do have to wait for /// files which no longer belong to the data source, and the ones which now do have to wait for
/// the next run. /// the next run.
/// ///
/// Only a local folder chooses its documents by file patterns, exclusions, switches, and a size /// Local files and folders include their configured paths, so relocating one stops a run which
/// limit. For every other kind of data source, this is empty. /// still reads the old location. Folders also include their file selection. Neither changes the
/// embedding signature: unchanged documents keep their vectors.
/// </remarks> /// </remarks>
/// <param name="dataSource">The data source to index.</param> /// <param name="dataSource">The data source to index.</param>
/// <returns>The description of its selection, or an empty string.</returns> /// <returns>The description of its selection, or an empty string.</returns>
internal static string BuildSelectionSignature(IIndexedDataSource dataSource) => dataSource is DataSourceLocalDirectory directory internal static string BuildSelectionSignature(IIndexedDataSource dataSource) => dataSource switch
? FolderFileSelection.Of(directory).Describe() {
: string.Empty; DataSourceLocalDirectory directory => $"{directory.Path.Length}:{directory.Path}|{FolderFileSelection.Of(directory).Describe()}",
DataSourceLocalFile file => $"{file.FilePath.Length}:{file.FilePath}",
_ => string.Empty,
};
/// <summary> /// <summary>
/// Describes the indexing run a data source would get with a given embedding provider. /// Describes the indexing run a data source would get with a given embedding provider.
@@ -190,24 +190,20 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
} }
/// <summary> /// <summary>
/// Whether the file or folder a data source reads must stay as it is. /// Whether a mailbox's origin must stay as it is.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Locked as soon as the index holds anything, because where a data source reads from is what it /// Locked as soon as the index holds anything: another server, account, or folder holds other
/// is: another folder is another data source, and the path reaches no signature, so swapping it /// mails, which makes it another data source. Local files and folders are not locked, since they
/// would leave the stored index describing documents nobody points at any more. /// may move, and the file indexer finds their documents again, cf. FileSourceIndexer.RelocateStoredFilesAsync.
/// /// The embedding provider is never locked; DataSourceReindexWarning asks what changing it costs.
/// The embedding provider used to be locked along with it and no longer is. It does reach the
/// signature, so changing it rebuilds the index cleanly -- and DataSourceReindexWarning asks
/// before it does. Locking it as well left a data source whose provider was deleted stuck on
/// keyword search for good, with no way back.
/// ///
/// Unclear counts as locked: an unavailable index database says nothing about what is stored. /// Unclear counts as locked: an unavailable index database says nothing about what is stored.
/// </remarks> /// </remarks>
/// <param name="dataSourceId">The data source to ask about.</param> /// <param name="dataSourceId">The mailbox to ask about.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>True when the source must not be changed.</returns> /// <returns>True when the source must not be changed.</returns>
public async Task<bool> ShouldLockDataSourceOriginAsync(string dataSourceId, CancellationToken token = default) public async Task<bool> ShouldLockMailboxOriginAsync(string dataSourceId, CancellationToken token = default)
{ {
var indexStore = await databaseClientProvider.GetIndexStoreAsync(token); var indexStore = await databaseClientProvider.GetIndexStoreAsync(token);
if (!indexStore.IsAvailable) if (!indexStore.IsAvailable)
@@ -17,20 +17,25 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
if (assistantPlugin.ChatLaunchConfiguration is not { } launchConfiguration) if (assistantPlugin.ChatLaunchConfiguration is not { } launchConfiguration)
return new(null, TB("The assistant plugin does not contain a valid chat launch configuration.")); return new(null, TB("The assistant plugin does not contain a valid chat launch configuration."));
var providerResult = this.ResolveProvider(launchConfiguration.ProviderId); //
if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE) // The chat template comes first, because it may preselect the provider and the profile as
return new(null, providerResult.ErrorMessage); // well. When it does, it wins over the launcher — the same rule as for the tools and the
// data sources, so that nobody has to remember two of them:
var profileResult = this.ResolveProfile(launchConfiguration.ProfileId); //
var profile = profileResult.Profile;
if (profile is null)
return new(null, profileResult.ErrorMessage);
var chatTemplateResult = this.ResolveChatTemplate(launchConfiguration.ChatTemplateId); var chatTemplateResult = this.ResolveChatTemplate(launchConfiguration.ChatTemplateId);
var chatTemplate = chatTemplateResult.ChatTemplate; var chatTemplate = chatTemplateResult.ChatTemplate;
if (chatTemplate is null) if (chatTemplate is null)
return new(null, chatTemplateResult.ErrorMessage); return new(null, chatTemplateResult.ErrorMessage);
var providerResult = this.ResolveProvider(assistantPlugin.Name, chatTemplate, launchConfiguration.ProviderId);
if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE)
return new(null, providerResult.ErrorMessage);
var profileResult = this.ResolveProfile(assistantPlugin.Name, chatTemplate, launchConfiguration.ProfileId);
var profile = profileResult.Profile;
if (profile is null)
return new(null, profileResult.ErrorMessage);
// //
// A chat template that forbids profiles wins over a configured profile: the chat disables // A chat template that forbids profiles wins over a configured profile: the chat disables
// its profile selection for such templates, so keeping the profile would pin one that the // its profile selection for such templates, so keeping the profile would pin one that the
@@ -52,8 +57,10 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
// //
// A launcher that names no workspace wants the same chat the chat page starts on its own: // A launcher that names no workspace wants the same chat the chat page starts on its own:
// one that belongs nowhere, is kept among the temporary chats, and disappears with them. The // one that belongs nowhere, is kept among the temporary chats, and disappears with them —
// empty workspace ID is what says so, here as everywhere else in the app. // unless its chat template names a workspace, which then fills in, just like on the chat
// page. A workspace the launcher names itself was chosen on purpose and wins. The empty
// workspace ID is what says "no workspace", here as everywhere else in the app.
// //
var workspaceId = Guid.Empty; var workspaceId = Guid.Empty;
if (!launchConfiguration.OpensTemporaryChat) if (!launchConfiguration.OpensTemporaryChat)
@@ -74,6 +81,11 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
return new(null, string.Format(TB("The workspace '{0}' could not be opened or created."), launchConfiguration.WorkspaceName)); return new(null, string.Format(TB("The workspace '{0}' could not be opened or created."), launchConfiguration.WorkspaceName));
} }
} }
else if (settingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } templateWorkspaceName)
{
// A workspace of the template which cannot be used does not stop the launch, the same as on the chat page:
workspaceId = await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(templateWorkspaceName, chatTemplate.GetSafeName());
}
var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds); var toolChoice = ChatTemplate.ChooseToolIds(chatTemplate, launchConfiguration.ToolIds);
if (toolChoice.LauncherChoiceDropped) if (toolChoice.LauncherChoiceDropped)
@@ -115,7 +127,34 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty); return new(new(chatThread, ApplySelectedChatTemplateToComposer: true, PreserveDataSourceOptions: dataSourcesWereChosen), string.Empty);
} }
private (ProviderSettings Provider, bool IsExplicit, string ErrorMessage) ResolveProvider(Guid? providerId) /// <remarks>
/// A provider of the chat template which is gone, or which may not be used for chats, does not
/// stop the launch: the chat then starts as if the template named none, so the launcher decides
/// after all. Only what the launcher names itself is an error when it cannot be used, because
/// that is the one choice its author can fix in the launcher.
/// </remarks>
internal ChatLauncherProviderResult ResolveProvider(string pluginName, ChatTemplate chatTemplate, Guid? launcherProviderId)
{
var templateProvider = settingsManager.GetChatTemplateProvider(chatTemplate);
if (templateProvider != ProviderSettings.NONE)
{
if (launcherProviderId is not null)
logger.LogWarning(
"Assistant plugin '{PluginName}' selects the provider '{LauncherProviderId}', but its chat template '{ChatTemplateName}' preselects a provider of its own. The chat starts with the provider of that template.",
pluginName, launcherProviderId, chatTemplate.GetSafeName());
return new(templateProvider, true, string.Empty);
}
if (chatTemplate.ProviderId is not null)
logger.LogWarning(
"The chat template '{ChatTemplateName}' preselects the provider '{TemplateProviderId}', which does not exist or may not be used for chats. The chat starts as if the template named no provider.",
chatTemplate.GetSafeName(), chatTemplate.ProviderId);
return this.ResolveLauncherProvider(launcherProviderId);
}
private ChatLauncherProviderResult ResolveLauncherProvider(Guid? providerId)
{ {
// //
// The launcher does not name a provider, so it wants the chat defaults. We resolve them // The launcher does not name a provider, so it wants the chat defaults. We resolve them
@@ -140,7 +179,32 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
return new(provider, true, string.Empty); return new(provider, true, string.Empty);
} }
private (Profile? Profile, string ErrorMessage) ResolveProfile(Guid? profileId) /// <remarks>
/// The same as for the provider: a profile of the chat template which is gone leaves the choice
/// to the launcher. A template which does not allow profiles names none here, and the launch
/// below drops the profile of the launcher for it as well.
/// </remarks>
internal ChatLauncherProfileResult ResolveProfile(string pluginName, ChatTemplate chatTemplate, Guid? launcherProfileId)
{
if (settingsManager.GetChatTemplateProfile(chatTemplate) is { } templateProfile)
{
if (launcherProfileId is not null)
logger.LogWarning(
"Assistant plugin '{PluginName}' selects the profile '{LauncherProfileId}', but its chat template '{ChatTemplateName}' preselects a profile of its own. The chat starts with the profile of that template.",
pluginName, launcherProfileId, chatTemplate.GetSafeName());
return new(templateProfile, string.Empty);
}
if (chatTemplate.AllowProfileUsage && chatTemplate.ProfileId is not null)
logger.LogWarning(
"The chat template '{ChatTemplateName}' preselects the profile '{TemplateProfileId}', which does not exist. The chat starts as if the template named no profile.",
chatTemplate.GetSafeName(), chatTemplate.ProfileId);
return this.ResolveLauncherProfile(launcherProfileId);
}
private ChatLauncherProfileResult ResolveLauncherProfile(Guid? profileId)
{ {
if (profileId is null) if (profileId is null)
return new(settingsManager.GetPreselectedProfile(Components.CHAT), string.Empty); return new(settingsManager.GetPreselectedProfile(Components.CHAT), string.Empty);
@@ -159,7 +223,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
: new(profile, string.Empty); : new(profile, string.Empty);
} }
private (ChatTemplate? ChatTemplate, string ErrorMessage) ResolveChatTemplate(Guid? chatTemplateId) private ChatLauncherTemplateResult ResolveChatTemplate(Guid? chatTemplateId)
{ {
if (chatTemplateId is null) if (chatTemplateId is null)
return new(settingsManager.GetPreselectedChatTemplate(Components.CHAT), string.Empty); return new(settingsManager.GetPreselectedChatTemplate(Components.CHAT), string.Empty);
@@ -178,7 +242,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
: new(chatTemplate, string.Empty); : new(chatTemplate, string.Empty);
} }
private async Task<(DataSourceOptions? Options, string ErrorMessage)> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList<Guid>? launcherDataSourceIds) private async Task<ChatLauncherDataSourcesResult> ResolveDataSourceOptionsAsync(PluginAssistants assistantPlugin, ProviderSettings provider, ChatTemplate chatTemplate, IReadOnlyList<Guid>? launcherDataSourceIds)
{ {
// //
// The launcher names data sources as plain IDs, and the options around them are always the // The launcher names data sources as plain IDs, and the options around them are always the
@@ -225,8 +289,17 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
/// <param name="chosenOptions">The options the chat is about to start with.</param> /// <param name="chosenOptions">The options the chat is about to start with.</param>
/// <param name="originChatTemplate">The chat template the options came from, or null when the launcher named the sources itself.</param> /// <param name="originChatTemplate">The chat template the options came from, or null when the launcher named the sources itself.</param>
/// <returns>The checked options, or null and a message saying why no chat was created.</returns> /// <returns>The checked options, or null and a message saying why no chat was created.</returns>
private async Task<(DataSourceOptions? Options, string ErrorMessage)> CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate) internal async Task<ChatLauncherDataSourcesResult> CheckChosenDataSourcesAsync(ProviderSettings provider, DataSourceOptions chosenOptions, ChatTemplate? originChatTemplate)
{ {
//
// Without the RAG preview, the chat uses no data sources at all, so a source which is gone
// or not permitted takes nothing from it. Stopping the launch for it would block a launcher
// over something the user can neither see nor change. The options stay in the chat as they
// are, just like a chat template keeps them, and count once RAG is switched on again.
//
if (!PreviewFeatures.PRE_RAG_2024.IsEnabled(settingsManager))
return new(chosenOptions, string.Empty);
// //
// There is nothing to check when data sources are switched off, and nothing to check either // There is nothing to check when data sources are switched off, and nothing to check either
// when an agent picks them: that choice is made per message in the chat, exactly as it is // when an agent picks them: that choice is made per message in the chat, exactly as it is
@@ -1,3 +1,4 @@
namespace AIStudio.Tools.Services; namespace AIStudio.Tools.Services;
public sealed record EmbeddedFileRecord(string Fingerprint, long FileSize, DateTimeOffset LastWriteUtc, DateTimeOffset EmbeddedAtUtc, int ChunkCount, string? TranscriptKey = null); public sealed record EmbeddedFileRecord(string Fingerprint, long FileSize, DateTimeOffset LastWriteUtc, DateTimeOffset EmbeddedAtUtc, int ChunkCount, string? TranscriptKey = null,
string? RelativePath = null, string? ParentFileId = null);
@@ -226,7 +226,7 @@ internal sealed class DocumentRunProgress(IndexedRunContext context, int totalDo
var embeddedAtUtc = DateTimeOffset.UtcNow; var embeddedAtUtc = DateTimeOffset.UtcNow;
var state = document.State with { ChunkCount = chunkCount, EmbeddedAtUtc = embeddedAtUtc }; var state = document.State with { ChunkCount = chunkCount, EmbeddedAtUtc = embeddedAtUtc };
await context.IndexStore.UpsertFileAsync(context.DataSource.Id, state, token); await context.IndexStore.UpsertFileAsync(context.DataSource.Id, state, token);
context.Manifest.Files[document.Key] = new EmbeddedFileRecord(state.Fingerprint, state.FileSize, state.LastWriteUtc, embeddedAtUtc, chunkCount, state.TranscriptKey); context.Manifest.Files[document.Key] = new EmbeddedFileRecord(state.Fingerprint, state.FileSize, state.LastWriteUtc, embeddedAtUtc, chunkCount, state.TranscriptKey, state.RelativePath, state.ParentFileId);
await context.ForgetPermanentFailureAsync(document.Key, token); await context.ForgetPermanentFailureAsync(document.Key, token);
context.Pass.Forget(document.Key); context.Pass.Forget(document.Key);
@@ -0,0 +1,91 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services.Indexing;
internal sealed partial class FileSourceIndexer
{
/// <summary>
/// Reconnects unchanged files before enumeration and missing-file cleanup. In particular, media
/// must already be known under their new paths when enumeration asks whether they need transcription.
/// </summary>
private async Task<bool> RelocateStoredFilesAsync(IndexedRunContext context, IDataSource dataSource, CancellationToken token)
{
DocumentRunProgress? progress = null;
foreach (var (oldPath, record) in context.Manifest.Files.ToArray())
{
token.ThrowIfCancellationRequested();
var file = FindRelocatedFile(dataSource, oldPath, record);
if (file is null || record.ParentFileId is not { } parentFileId || (!DataSourceEmbeddingManifest.PATH_COMPARER.Equals(oldPath, file.FullName) && context.Manifest.Files.ContainsKey(file.FullName)))
continue;
progress ??= DocumentRunProgress.FromStoredState(context, context.Manifest.Files.Count, logger);
if (progress.HasDoneItsShare(DateTimeOffset.UtcNow))
return false;
progress.Publish();
var relativePath = this.TryGetRelativePath(dataSource, file);
// Use the metadata which passed the reuse check. If the file changes during these
// writes, enumeration must see a different fingerprint and index its new content.
var fingerprint = BuildFileMetadataHash(file.FullName, file.Name, record.LastWriteUtc.UtcDateTime, record.FileSize);
// Vectors first, then SQLite. Selecting by the unchanged parent id makes this safe to
// retry if cancellation or a crash falls between the two writes. A failure aborts the
// run before missing-file cleanup can remove the documents we are reconnecting.
await context.VectorStore.RelocateEmbeddingFileAsync(context.CollectionName, parentFileId, file.FullName, relativePath, fingerprint, token);
token.ThrowIfCancellationRequested();
await context.IndexStore.RelocateFileAsync(dataSource.Id, parentFileId, file.FullName, relativePath, fingerprint, token);
context.Manifest.Files.Remove(oldPath);
context.Manifest.Files[file.FullName] = record with { Fingerprint = fingerprint, RelativePath = relativePath };
logger.LogInformation("Reused stored embeddings for relocated file '{OldPath}' at '{NewPath}' in data source '{DataSourceId}'.", oldPath, file.FullName, dataSource.Id);
}
return true;
}
/// <summary>
/// Uses the same size and modification-time check as ordinary indexing, matching a folder's
/// documents by relative path. It never searches another folder for a missing document.
/// </summary>
/// <remarks>
/// A stored path which still lies below the selected folder stays as it is: it points at its
/// file, and a file which is gone from there is gone. Mapping it through its old relative path
/// would move it, e.g., when the selected folder grows to its parent. A copy at the mapped place
/// would then take over the identity of the document while the original is indexed anew.
/// Checking this first also spares every run of an unmoved folder resolving each stored path.
/// </remarks>
internal static FileInfo? FindRelocatedFile(IDataSource dataSource, string oldPath, EmbeddedFileRecord record)
{
if (record.ParentFileId is null)
return null;
string targetPath;
switch (dataSource)
{
case DataSourceLocalFile localFile when RelocatedFilePaths.IsLocalAbsolute(localFile.FilePath):
targetPath = Path.GetFullPath(localFile.FilePath);
break;
case DataSourceLocalDirectory localDirectory when RelocatedFilePaths.IsBelow(localDirectory.Path, oldPath):
return null;
case DataSourceLocalDirectory localDirectory:
var resolvedPath = RelocatedFilePaths.Resolve(localDirectory.Path, oldPath, record.RelativePath);
if (resolvedPath is null)
return null;
targetPath = resolvedPath;
break;
default:
return null;
}
if (string.Equals(oldPath, targetPath, StringComparison.Ordinal)
|| !string.Equals(Path.GetExtension(RelocatedFilePaths.GetStoredFileName(oldPath)), Path.GetExtension(targetPath), StringComparison.OrdinalIgnoreCase))
return null;
var file = new FileInfo(targetPath);
return file.Exists && file.Length == record.FileSize && file.LastWriteTimeUtc == record.LastWriteUtc.UtcDateTime ? file : null;
}
}
@@ -251,7 +251,7 @@ internal sealed partial class FileSourceIndexer
return true; return true;
if (configuration.TargetFileName is { } targetFileName) if (configuration.TargetFileName is { } targetFileName)
return string.Equals(fileName, targetFileName, StringComparison.OrdinalIgnoreCase); return string.Equals(fileName, targetFileName, PathTools.COMPARISON);
// //
// Git renames .git/index.lock to .git/index with almost every command, and a build or // Git renames .git/index.lock to .git/index with almost every command, and a build or
@@ -297,13 +297,13 @@ internal sealed partial class FileSourceIndexer
private static DataSourceWatcherConfiguration? GetWatchConfiguration(IDataSourceBase dataSource) => dataSource switch private static DataSourceWatcherConfiguration? GetWatchConfiguration(IDataSourceBase dataSource) => dataSource switch
{ {
DataSourceLocalDirectory localDirectory when Directory.Exists(localDirectory.Path) => new DataSourceWatcherConfiguration( DataSourceLocalDirectory localDirectory when RelocatedFilePaths.IsLocalAbsolute(localDirectory.Path) && Directory.Exists(localDirectory.Path) => new DataSourceWatcherConfiguration(
localDirectory.Path, localDirectory.Path,
localDirectory.IncludeSubdirectories, localDirectory.IncludeSubdirectories,
localDirectory.IncludeHiddenFilesAndFolders, localDirectory.IncludeHiddenFilesAndFolders,
FilePatterns: FileNamePatterns.Parse(localDirectory.EffectiveFilePatterns), FilePatterns: FileNamePatterns.Parse(localDirectory.EffectiveFilePatterns),
ExcludePatterns: FileExclusions.Parse(localDirectory.EffectiveExcludePatterns)), ExcludePatterns: FileExclusions.Parse(localDirectory.EffectiveExcludePatterns)),
DataSourceLocalFile localFile when File.Exists(localFile.FilePath) && !string.IsNullOrWhiteSpace(Path.GetDirectoryName(localFile.FilePath)) => new DataSourceWatcherConfiguration( DataSourceLocalFile localFile when RelocatedFilePaths.IsLocalAbsolute(localFile.FilePath) && File.Exists(localFile.FilePath) && !string.IsNullOrWhiteSpace(Path.GetDirectoryName(localFile.FilePath)) => new DataSourceWatcherConfiguration(
Path.GetDirectoryName(localFile.FilePath)!, Path.GetDirectoryName(localFile.FilePath)!,
false, false,
TargetFileName: Path.GetFileName(localFile.FilePath)), TargetFileName: Path.GetFileName(localFile.FilePath)),
@@ -314,8 +314,8 @@ internal sealed partial class FileSourceIndexer
{ {
return left.IncludeSubdirectories == right.IncludeSubdirectories return left.IncludeSubdirectories == right.IncludeSubdirectories
&& left.IncludeHiddenFilesAndFolders == right.IncludeHiddenFilesAndFolders && left.IncludeHiddenFilesAndFolders == right.IncludeHiddenFilesAndFolders
&& string.Equals(left.RootPath, right.RootPath, StringComparison.OrdinalIgnoreCase) && string.Equals(left.RootPath, right.RootPath, PathTools.COMPARISON)
&& string.Equals(left.TargetFileName, right.TargetFileName, StringComparison.OrdinalIgnoreCase) && string.Equals(left.TargetFileName, right.TargetFileName, PathTools.COMPARISON)
&& FileNamePatterns.AreEquivalent(left.FilePatterns, right.FilePatterns) && FileNamePatterns.AreEquivalent(left.FilePatterns, right.FilePatterns)
&& FileNamePatterns.AreEquivalent(left.ExcludePatterns, right.ExcludePatterns); && FileNamePatterns.AreEquivalent(left.ExcludePatterns, right.ExcludePatterns);
} }
@@ -77,12 +77,25 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
var manifest = context.Manifest; var manifest = context.Manifest;
if (!await this.RelocateStoredFilesAsync(context, dataSource, token))
return await DocumentRunProgress.FromStoredState(context, manifest.Files.Count, logger).PauseRunAsync(null, "file relocation paused", token);
var enumerationStartedUtc = DateTimeOffset.UtcNow; var enumerationStartedUtc = DateTimeOffset.UtcNow;
var inputFiles = this.GetInputFiles(dataSource, manifest); var inputFiles = this.GetInputFiles(dataSource, manifest);
var enumerationDuration = DateTimeOffset.UtcNow - enumerationStartedUtc; var enumerationDuration = DateTimeOffset.UtcNow - enumerationStartedUtc;
if (inputFiles.UnreachableRootReason is { } unreachableRootReason) if (inputFiles.UnreachableRootReason is { } unreachableRootReason)
return this.StopWithoutRoot(context, dataSource, unreachableRootReason); return this.StopWithoutRoot(context, dataSource, unreachableRootReason);
// An unmounted volume can leave a readable, empty mount directory. There is no way to
// distinguish that from intentionally removing every file, so keep the stored index.
// Explicit exclusions still take effect: they are evidence of a nonempty folder.
if (dataSource is DataSourceLocalDirectory directory
&& (manifest.Files.Count > 0 || manifest.UnfinishedFiles.Count > 0 || manifest.PermanentFailures.Count > 0)
&& inputFiles.Files.Count == 0 && inputFiles.IgnoredFiles.Count == 0
&& inputFiles.FilePatternExclusionCount == 0 && inputFiles.ExcludedFolderCount == 0
&& inputFiles.FailedFiles == 0 && inputFiles.UnreadableDirectories.Count == 0)
return this.StopWithoutRoot(context, dataSource, string.Format(TB("The folder '{0}' currently appears empty. AI Studio kept the index of this data source, because the folder of a drive that is not connected can look empty. Connect the drive and refresh this data source. If you emptied the folder on purpose, remove this data source."), directory.Path));
var indexedFiles = inputFiles.Files; var indexedFiles = inputFiles.Files;
var totalFiles = indexedFiles.Count + inputFiles.FailedFiles + inputFiles.IgnoredFiles.Count; var totalFiles = indexedFiles.Count + inputFiles.FailedFiles + inputFiles.IgnoredFiles.Count;
@@ -364,7 +377,8 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
/// connected, or a network share while the VPN is down, would remove every embedding of the /// connected, or a network share while the VPN is down, would remove every embedding of the
/// data source, and embedding it all again once the folder is back costs as long as it took the /// data source, and embedding it all again once the folder is back costs as long as it took the
/// first time. Nothing is written: the stored hash still tells which state of the data source /// first time. Nothing is written: the stored hash still tells which state of the data source
/// the index holds, so the next run picks up from there. /// the index holds, so the next run picks up from there. The status asks for attention, so the
/// embeddings page offers to edit the data source, e.g. to select the new location of a moved folder.
/// </remarks> /// </remarks>
/// <param name="context">The run.</param> /// <param name="context">The run.</param>
/// <param name="dataSource">The data source.</param> /// <param name="dataSource">The data source.</param>
@@ -378,7 +392,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
dataSource.Id, dataSource.Id,
context.Manifest.Files.Count); context.Manifest.Files.Count);
DocumentRunProgress.FromStoredState(context, context.Manifest.Files.Count, logger).PublishRunFailure(reason); DocumentRunProgress.FromStoredState(context, context.Manifest.Files.Count, logger).PublishRunFailure(reason, DataSourceAttention.LOCATION_UNREACHABLE);
return IndexedRunOutcome.DONE; return IndexedRunOutcome.DONE;
} }
@@ -509,9 +523,10 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
/// user may not read that folder. Removing it would throw away its index, and embedding it again /// user may not read that folder. Removing it would throw away its index, and embedding it again
/// once the folder can be read costs as long as it took the first time. /// once the folder can be read costs as long as it took the first time.
/// <br /><br /> /// <br /><br />
/// Paths are compared ignoring their case, as everywhere in the manifest. On Linux and macOS, that /// Paths are compared ignoring their case, as everywhere in the index, cf. the remarks on
/// keeps a file whose path differs from an unreadable folder in its case alone, which costs no more /// DataSourceEmbeddingManifest.PATH_COMPARER. On Linux and macOS, that keeps a file whose path
/// than a stale entry until the folder can be read again. /// differs from an unreadable folder in its case alone, which costs no more than a stale entry
/// until the folder can be read again.
/// </remarks> /// </remarks>
/// <param name="storedPaths">The paths of the files the index knows about.</param> /// <param name="storedPaths">The paths of the files the index knows about.</param>
/// <param name="files">The files this run found.</param> /// <param name="files">The files this run found.</param>
@@ -521,15 +536,16 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
{ {
var existingPaths = files var existingPaths = files
.Select(file => file.FullName) .Select(file => file.FullName)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(DataSourceEmbeddingManifest.PATH_COMPARER);
var unreadablePrefixes = unreadableDirectories var unreadablePrefixes = unreadableDirectories
.Select(directory => Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory)) + Path.DirectorySeparatorChar) .Select(Path.GetFullPath)
.Select(directory => Path.EndsInDirectorySeparator(directory) ? directory : directory + Path.DirectorySeparatorChar)
.ToList(); .ToList();
return storedPaths return storedPaths
.Where(path => !existingPaths.Contains(path)) .Where(path => !existingPaths.Contains(path))
.Where(path => !unreadablePrefixes.Any(prefix => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) .Where(path => !unreadablePrefixes.Any(prefix => path.StartsWith(prefix, DataSourceEmbeddingManifest.PATH_COMPARISON)))
.ToList(); .ToList();
} }
@@ -709,7 +725,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
switch (dataSource) switch (dataSource)
{ {
case DataSourceLocalFile localFile when File.Exists(localFile.FilePath): case DataSourceLocalFile localFile when RelocatedFilePaths.IsLocalAbsolute(localFile.FilePath) && File.Exists(localFile.FilePath):
var file = new FileInfo(localFile.FilePath); var file = new FileInfo(localFile.FilePath);
// No size limit: the user chose this very file, cf. FolderFileSelection.IsTooLarge. // No size limit: the user chose this very file, cf. FolderFileSelection.IsTooLarge.
@@ -718,9 +734,10 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
return result; return result;
case DataSourceLocalDirectory localDirectory when Directory.Exists(localDirectory.Path): case DataSourceLocalDirectory localDirectory when RelocatedFilePaths.IsLocalAbsolute(localDirectory.Path) && Directory.Exists(localDirectory.Path):
this.EnumerateAccessibleFiles(localDirectory, manifest, mediaTranscriptionAvailability, result); this.EnumerateAccessibleFiles(localDirectory, manifest, mediaTranscriptionAvailability, result);
if (result.UnreadableDirectories.Contains(localDirectory.Path)) if (result.UnreadableDirectories.Any(directory => string.Equals(Path.TrimEndingDirectorySeparator(Path.GetFullPath(directory)),
Path.TrimEndingDirectorySeparator(Path.GetFullPath(localDirectory.Path)), PathTools.COMPARISON)))
result.UnreachableRootReason = string.Format(TB("The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index."), localDirectory.Path); result.UnreachableRootReason = string.Format(TB("The folder '{0}' could not be opened. Please check whether you are allowed to read it. Nothing was removed from the index."), localDirectory.Path);
return result; return result;
@@ -732,11 +749,11 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
switch (dataSource) switch (dataSource)
{ {
case DataSourceLocalFile localFile: case DataSourceLocalFile localFile:
result.UnreachableRootReason = string.Format(TB("The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. Once the file is back, refresh this data source. When it is gone for good, remove this data source."), localFile.FilePath); result.UnreachableRootReason = string.Format(TB("The file '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the file was moved or renamed. Nothing was removed from the index. If the file was moved or renamed, edit this data source to select its new location. Once the file is back, refresh this data source. When it is gone for good, remove this data source."), localFile.FilePath);
break; break;
case DataSourceLocalDirectory localDirectory: case DataSourceLocalDirectory localDirectory:
result.UnreachableRootReason = string.Format(TB("The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. Once the folder is back, refresh this data source."), localDirectory.Path); result.UnreachableRootReason = string.Format(TB("The folder '{0}' cannot be reached. Perhaps the drive it is on is not connected, or the folder was moved or renamed. Nothing was removed from the index. If the folder was moved or renamed, edit this data source to select its new location. Once the folder is back, refresh this data source."), localDirectory.Path);
break; break;
} }
@@ -935,8 +952,8 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
private DataSourceMetadataSnapshot BuildDataSourceMetadataSnapshot(IDataSource dataSource, IReadOnlyList<FileInfo> indexedFiles) private DataSourceMetadataSnapshot BuildDataSourceMetadataSnapshot(IDataSource dataSource, IReadOnlyList<FileInfo> indexedFiles)
{ {
var fileHashes = indexedFiles var fileHashes = indexedFiles
.OrderBy(file => file.FullName, StringComparer.OrdinalIgnoreCase) .OrderBy(file => file.FullName, DataSourceEmbeddingManifest.PATH_COMPARER)
.ToDictionary(file => file.FullName, BuildFileMetadataHash, StringComparer.OrdinalIgnoreCase); .ToDictionary(file => file.FullName, BuildFileMetadataHash, DataSourceEmbeddingManifest.PATH_COMPARER);
var sourceHash = dataSource switch var sourceHash = dataSource switch
{ {
@@ -980,7 +997,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
indexedFiles.Count.ToString() indexedFiles.Count.ToString()
}; };
foreach (var file in indexedFiles.OrderBy(file => file.FullName, StringComparer.OrdinalIgnoreCase)) foreach (var file in indexedFiles.OrderBy(file => file.FullName, DataSourceEmbeddingManifest.PATH_COMPARER))
{ {
parts.Add(this.TryGetRelativePath(dataSource, file)); parts.Add(this.TryGetRelativePath(dataSource, file));
parts.Add(fileHashes[file.FullName]); parts.Add(fileHashes[file.FullName]);
@@ -1002,14 +1019,24 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
"0"); "0");
} }
return BuildMetadataHash( return BuildFileMetadataHash(file.FullName, file.Name, file.LastWriteTimeUtc, file.Length);
"file",
file.FullName,
file.Name,
file.LastWriteTimeUtc.Ticks.ToString(),
file.Length.ToString());
} }
/// <summary>
/// The fingerprint of a file which exists, from its write time and size as they were read.
/// </summary>
/// <remarks>
/// The one formula for both an indexed file and a relocated one: if the two drifted apart, every
/// relocated file would look changed to the next run and be embedded again.
/// </remarks>
/// <param name="fullName">The absolute path of the file.</param>
/// <param name="name">The name of the file.</param>
/// <param name="lastWriteUtc">When the file was last written to.</param>
/// <param name="length">The size of the file in bytes.</param>
/// <returns>The fingerprint.</returns>
private static string BuildFileMetadataHash(string fullName, string name, DateTime lastWriteUtc, long length) =>
BuildMetadataHash("file", fullName, name, lastWriteUtc.Ticks.ToString(), length.ToString());
private static string BuildMetadataHash(params string[] parts) private static string BuildMetadataHash(params string[] parts)
{ {
return BuildMetadataHash((IEnumerable<string>)parts); return BuildMetadataHash((IEnumerable<string>)parts);
@@ -1036,7 +1063,7 @@ internal sealed partial class FileSourceIndexer(SettingsManager settingsManager,
/// <returns>The document.</returns> /// <returns>The document.</returns>
private EmbeddingDocument CreateFileDocument(IndexedRunContext context, IDataSource dataSource, FileInfo file, string fingerprint, MediaTranscriptSlot? transcript) => new( private EmbeddingDocument CreateFileDocument(IndexedRunContext context, IDataSource dataSource, FileInfo file, string fingerprint, MediaTranscriptSlot? transcript) => new(
file.FullName, file.FullName,
this.CreateEmbeddingStateFile(dataSource, file, fingerprint, 0, DateTimeOffset.UtcNow) with { TranscriptKey = transcript?.Key }, this.CreateEmbeddingStateFile(dataSource, file, fingerprint, 0, DateTimeOffset.UtcNow) with { ParentFileId = context.GetFileParentId(file.FullName), TranscriptKey = transcript?.Key },
file.Name, file.Name,
token => this.StreamEmbeddingChunksAsync(file.FullName, transcript, dataSource, context.EmbeddingProvider, context.PromptInjectionScope, token)); token => this.StreamEmbeddingChunksAsync(file.FullName, transcript, dataSource, context.EmbeddingProvider, context.PromptInjectionScope, token));
@@ -7,9 +7,11 @@ namespace AIStudio.Tools.Services.Indexing;
/// The ids under which documents and their chunks are stored. /// The ids under which documents and their chunks are stored.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Derived rather than random, so the same document of the same data source always ends up under /// Derived rather than random, so the same document of the same data source ends up under the same
/// the same ids, in the vector store as well as in the index store. The formats must stay as they /// ids, in the vector store as well as in the index store. Once stored, they are the identity of the
/// are: every stored id was made with them. /// document: a file which moves to another folder keeps its ids, although they were derived from its
/// old path and its old fingerprint, cf. FileSourceIndexer.RelocateStoredFilesAsync. The formats must
/// stay as they are: every stored id was made with them.
/// </remarks> /// </remarks>
internal static class IndexedDocumentIds internal static class IndexedDocumentIds
{ {
@@ -17,11 +19,31 @@ internal static class IndexedDocumentIds
/// The id of one chunk, which is also the id of its point in the vector store. /// The id of one chunk, which is also the id of its point in the vector store.
/// </summary> /// </summary>
/// <param name="dataSourceId">The data source the document belongs to.</param> /// <param name="dataSourceId">The data source the document belongs to.</param>
/// <param name="fingerprint">The fingerprint of the document as it was read.</param> /// <param name="source">What the chunk ids of the document derive from, cf. CreateChunkIdSource.</param>
/// <param name="chunkIndex">The position of the chunk within the document.</param> /// <param name="chunkIndex">The position of the chunk within the document.</param>
/// <returns>The chunk id.</returns> /// <returns>The chunk id.</returns>
public static string CreateChunkId(string dataSourceId, string fingerprint, int chunkIndex) => public static string CreateChunkId(string dataSourceId, string source, int chunkIndex) =>
CreateStableGuid($"{dataSourceId}:chunk:{fingerprint}:{chunkIndex}"); CreateStableGuid($"{dataSourceId}:chunk:{source}:{chunkIndex}");
/// <summary>
/// What the chunk ids of a document derive from: nearly always its fingerprint.
/// </summary>
/// <remarks>
/// A moved file keeps its parent id and its chunk ids, but its fingerprint now names the new path.
/// Another file may later appear at the old path with exactly the fingerprint those chunk ids came
/// from, and the fingerprint alone would hand it the same ids. The two documents would then take
/// over each other's chunks. Hence the parent id joins whenever a document is not stored under the
/// id its key gives, which only happens after a move. Every other document keeps the ids it has.
/// </remarks>
/// <param name="dataSourceId">The data source the document belongs to.</param>
/// <param name="documentKey">The key of the document.</param>
/// <param name="parentId">The id the document is stored under.</param>
/// <param name="fingerprint">The fingerprint of the document as it is about to be read.</param>
/// <returns>What to pass to CreateChunkId.</returns>
public static string CreateChunkIdSource(string dataSourceId, string documentKey, string parentId, string fingerprint) =>
string.Equals(parentId, CreateParentId(dataSourceId, documentKey), StringComparison.Ordinal)
? fingerprint
: $"{parentId}:{fingerprint}";
/// <summary> /// <summary>
/// The id of a document's row in the index store, which its chunks point at. /// The id of a document's row in the index store, which its chunks point at.
@@ -41,6 +41,9 @@ internal sealed class IndexedRunContext(IIndexedDataSource dataSource, Embedding
private long storedChunksSinceLastOptimization; private long storedChunksSinceLastOptimization;
private bool hasPendingChanges; private bool hasPendingChanges;
private Dictionary<string, string>? fileParentIds;
private HashSet<string>? reservedFileParentIds;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IndexedRunContext).Namespace, nameof(IndexedRunContext)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(IndexedRunContext).Namespace, nameof(IndexedRunContext));
/// <summary> /// <summary>
@@ -66,6 +69,38 @@ internal sealed class IndexedRunContext(IIndexedDataSource dataSource, Embedding
public IndexingPass Pass => pass; public IndexingPass Pass => pass;
/// <summary>
/// The id a file is stored under: the one it already has, or a new one.
/// </summary>
/// <remarks>
/// A relocated file keeps the id it got when it was indexed first, which came from its path back
/// then. A new file may now appear at that very path, so the ids already stored are reserved, and
/// a new file whose derived id is taken gets a random one instead. Its chunk ids include that id,
/// cf. IndexedDocumentIds.CreateChunkIdSource.
/// </remarks>
/// <param name="documentKey">The key of the file, which is its absolute path.</param>
/// <returns>The parent id of the file.</returns>
public string GetFileParentId(string documentKey)
{
if (this.fileParentIds is null)
{
this.fileParentIds = manifest.Files
.Where(file => file.Value.ParentFileId is not null)
.ToDictionary(file => file.Key, file => file.Value.ParentFileId!, DataSourceEmbeddingManifest.PATH_COMPARER);
this.reservedFileParentIds = this.fileParentIds.Values.ToHashSet(StringComparer.Ordinal);
}
if (this.fileParentIds.TryGetValue(documentKey, out var storedId))
return storedId;
var parentId = IndexedDocumentIds.CreateParentId(dataSource.Id, documentKey);
while (!this.reservedFileParentIds!.Add(parentId))
parentId = Guid.NewGuid().ToString();
this.fileParentIds[documentKey] = parentId;
return parentId;
}
/// <summary> /// <summary>
/// The run as one action of the prompt injection filter. /// The run as one action of the prompt injection filter.
/// </summary> /// </summary>
@@ -124,10 +159,11 @@ internal sealed class IndexedRunContext(IIndexedDataSource dataSource, Embedding
var embeddingBatchSize = Math.Max(1, embeddingProvider.EffectiveEmbeddingBatchSize); var embeddingBatchSize = Math.Max(1, embeddingProvider.EffectiveEmbeddingBatchSize);
var batch = new List<EmbeddingChunkDraft>(embeddingBatchSize); var batch = new List<EmbeddingChunkDraft>(embeddingBatchSize);
var totalChunkCount = 0; var totalChunkCount = 0;
var chunkIdSource = IndexedDocumentIds.CreateChunkIdSource(dataSource.Id, document.Key, document.State.ParentFileId, document.State.Fingerprint);
await foreach (var chunk in document.StreamChunks(token)) await foreach (var chunk in document.StreamChunks(token))
{ {
batch.Add(new(IndexedDocumentIds.CreateChunkId(dataSource.Id, document.State.Fingerprint, totalChunkCount), chunk.Text, totalChunkCount, chunk.PageNumber)); batch.Add(new(IndexedDocumentIds.CreateChunkId(dataSource.Id, chunkIdSource, totalChunkCount), chunk.Text, totalChunkCount, chunk.PageNumber));
totalChunkCount++; totalChunkCount++;
reportBlockProgress(totalChunkCount, chunk.PageNumber); reportBlockProgress(totalChunkCount, chunk.PageNumber);
@@ -163,7 +199,10 @@ internal sealed class IndexedRunContext(IIndexedDataSource dataSource, Embedding
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
public async Task DeleteDocumentPointsAsync(string documentKey, CancellationToken token) public async Task DeleteDocumentPointsAsync(string documentKey, CancellationToken token)
{ {
await vectorStore.DeleteEmbeddingByFile(this.CollectionName, documentKey, token); // A relocated document keeps its original parent id. Delete by that identity as well as
// by path, including when a previous relocation stopped between its two database writes.
var parentId = manifest.Files.GetValueOrDefault(documentKey)?.ParentFileId ?? string.Empty;
await vectorStore.DeleteEmbeddingByFile(this.CollectionName, documentKey, parentId, token);
this.hasPendingChanges = true; this.hasPendingChanges = true;
} }
@@ -44,7 +44,7 @@ internal sealed class IndexingPass(DataSourceRunSignature runSignature)
/// <summary> /// <summary>
/// The documents which failed during this pass for a reason which may pass, by their keys. /// The documents which failed during this pass for a reason which may pass, by their keys.
/// </summary> /// </summary>
private readonly Dictionary<string, FailedDocument> failedDocuments = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, FailedDocument> failedDocuments = new(DataSourceEmbeddingManifest.PATH_COMPARER);
// //
// Which kinds of provider failure the user was already told about in this pass. A rejected API // Which kinds of provider failure the user was already told about in this pass. A rejected API
@@ -0,0 +1,85 @@
namespace AIStudio.Tools.Services.Indexing;
/// <summary>
/// Reads paths stored by the computer which created an index. Its path syntax may differ from
/// that of the computer restoring the backup; never resolve an old absolute path on this machine.
/// </summary>
internal static class RelocatedFilePaths
{
internal static bool IsWindowsAbsolute(string path) => path.StartsWith(@"\\", StringComparison.Ordinal)
|| (path.Length >= 3 && char.IsAsciiLetter(path[0]) && path[1] == ':' && path[2] is '\\' or '/');
internal static bool IsLocalAbsolute(string path) => !string.IsNullOrWhiteSpace(path) && Path.IsPathFullyQualified(path)
&& (OperatingSystem.IsWindows() || !IsWindowsAbsolute(path));
internal static string GetStoredFileName(string path)
{
var separator = IsWindowsAbsolute(path) ? Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\')) : path.LastIndexOf('/');
return path[(separator + 1)..];
}
/// <summary>
/// Whether a stored path still lies below a root, spelled exactly as the root is now.
/// </summary>
/// <remarks>
/// Compared ordinally, unlike PathTools.IsInsideDirectory: once the name of a folder changed in its
/// case alone, its stored paths no longer match the paths a run finds, on Windows as well.
/// </remarks>
internal static bool IsBelow(string root, string storedPath)
{
if (!IsLocalAbsolute(root))
return false;
try
{
var prefix = Path.GetFullPath(root);
if (!Path.EndsInDirectorySeparator(prefix))
prefix += Path.DirectorySeparatorChar;
return storedPath.StartsWith(prefix, StringComparison.Ordinal);
}
catch (Exception exception) when (exception is ArgumentException or NotSupportedException or PathTooLongException)
{
return false;
}
}
/// <summary>
/// Maps a stored relative path below a newly selected root, including drive roots and mounted
/// volumes. Backslashes are separators only for a Windows index: they are legal Unix file names.
/// </summary>
internal static string? Resolve(string root, string oldAbsolutePath, string? relativePath)
{
if (!IsLocalAbsolute(root) || string.IsNullOrWhiteSpace(relativePath))
return null;
var windowsSource = IsWindowsAbsolute(oldAbsolutePath);
if (!windowsSource && !oldAbsolutePath.StartsWith('/'))
return null;
var parts = relativePath.Split(windowsSource ? ['/', '\\'] : ['/']);
foreach (var part in parts)
{
// Check after decoding the source separators, so a Windows '..\\' cannot evade the
// containment check on Unix. Reject target-invalid names rather than changing them.
if (part is "" or "." or ".." || part.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
return null;
if (windowsSource && part.Contains(':'))
return null;
if (OperatingSystem.IsWindows() && (part.EndsWith('.') || part.EndsWith(' ')))
return null;
}
try
{
var target = Path.GetFullPath(Path.Combine(root, Path.Combine(parts)));
return PathTools.IsInsideDirectory(root, target) ? target : null;
}
catch (Exception exception) when (exception is ArgumentException or NotSupportedException or PathTooLongException)
{
return null;
}
}
}
@@ -0,0 +1,93 @@
using AIStudio.Chat;
using AIStudio.Tools.Rust;
using Microsoft.Extensions.Caching.Memory;
// ReSharper disable NotAccessedPositionalProperty.Local
namespace AIStudio.Tools.Services;
/// <summary>
/// Prepares the images which go to an AI model, and keeps the latest results for a while.
/// </summary>
/// <remarks>
/// Every message builds the whole conversation anew, with every image in it. Without the cache,
/// each photo of a chat would be decoded and encoded again for each message, which a small device
/// such as a Raspberry Pi notices. The cache also lets the Batch Processing assistant check an
/// image ahead of its request without preparing it twice.
///
/// The cache is a MemoryCache of its own: the shared one, which the Markdown library uses, has no
/// size limit, and its entries state no size.
/// </remarks>
/// <param name="rustService">The runtime which prepares the images.</param>
/// <param name="logger">The logger.</param>
public sealed class ModelImageService(RustService rustService, ILogger<ModelImageService> logger) : IDisposable
{
/// <summary>
/// How much memory the cached images may take: a 1 MB photo takes about 2.7 MB as base64 in a string.
/// </summary>
private const long CACHE_SIZE_LIMIT_BYTES = 32 * 1024 * 1024;
/// <summary>
/// How long a cached image waits for its next message. A chat paused for longer prepares its images again.
/// </summary>
private static readonly TimeSpan CACHE_SLIDING_EXPIRATION = TimeSpan.FromMinutes(10);
private readonly MemoryCache cache = new(new MemoryCacheOptions { SizeLimit = CACHE_SIZE_LIMIT_BYTES });
/// <summary>
/// Prepares an attached image for the model of the given provider.
/// </summary>
/// <remarks>
/// A changed file and another byte budget each make for a new preparation. An image which could
/// not be prepared is not cached, so the next message tries it again, e.g., once the network
/// drive holding it is back.
/// </remarks>
/// <param name="image">The attached image.</param>
/// <param name="provider">The configured provider the image goes to.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The prepared image, or why it could not be prepared.</returns>
public async Task<PreparedModelImage> PrepareAsync(FileAttachmentImage image, AIStudio.Settings.Provider provider, CancellationToken token = default)
{
var file = new FileInfo(image.FilePath);
if (!file.Exists)
{
logger.LogWarning("The image '{ImagePath}' does not exist anymore and cannot go to the model.", image.FilePath);
return PreparedModelImage.Failed(ModelImageErrorCode.FILE_NOT_FOUND);
}
var maxBytes = IImageSourceExtensions.MaxImageSizeBytesFor(provider);
var key = new CacheKey(file.FullName, file.LastWriteTimeUtc, file.Length, maxBytes);
if (this.cache.TryGetValue(key, out PreparedModelImage? cached) && cached is not null)
{
logger.LogInformation("Took the image '{ImagePath}' for a budget of {MaxBytes} bytes from the cache.", file.FullName, maxBytes);
return cached;
}
var prepared = await rustService.PrepareImageForModelAsync(file.FullName, maxBytes, token);
if (!prepared.IsPrepared)
{
logger.LogWarning("The image '{ImagePath}' could not be prepared for the model: {ErrorCode}.", file.FullName, prepared.ErrorCode);
return prepared;
}
//
// The runtime logs the sizes before and after, so this line only says where it came from:
//
logger.LogInformation("The runtime prepared the image '{ImagePath}' for a budget of {MaxBytes} bytes; it is cached now.", file.FullName, maxBytes);
this.cache.Set(key, prepared, new MemoryCacheEntryOptions
{
Size = prepared.Base64.Length * (long)sizeof(char),
SlidingExpiration = CACHE_SLIDING_EXPIRATION,
});
return prepared;
}
/// <inheritdoc />
public void Dispose() => this.cache.Dispose();
/// <summary>
/// What makes a prepared image valid: the file as it is now, and the budget it was made for.
/// </summary>
private readonly record struct CacheKey(string FullPath, DateTime LastWriteTimeUtc, long Length, long MaxBytes);
}
@@ -13,9 +13,10 @@ public sealed partial class RustService
async () => async () =>
{ {
var encodedTitle = Uri.EscapeDataString(title); var encodedTitle = Uri.EscapeDataString(title);
var result = initialDirectory is null var startDirectory = initialDirectory is null ? null : PathTools.FindNearestExistingPath(initialDirectory);
var result = startDirectory is null
? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null) ? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null)
: await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions); : await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(startDirectory), this.jsonRustSerializerOptions);
if (result.IsSuccessStatusCode) if (result.IsSuccessStatusCode)
return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions); return await result.Content.ReadFromJsonAsync<DirectorySelectionResponse>(this.jsonRustSerializerOptions);
@@ -54,10 +55,11 @@ public sealed partial class RustService
"select file", "select file",
async () => async () =>
{ {
var startPath = initialFile is null ? null : PathTools.FindNearestExistingPath(initialFile);
var payload = new SelectFileOptions var payload = new SelectFileOptions
{ {
Title = title, Title = title,
PreviousFile = initialFile is null ? null : new (initialFile), PreviousFile = startPath is null ? null : new (startPath),
Filter = FileTypes.ToFileDialogFilter(filter) Filter = FileTypes.ToFileDialogFilter(filter)
}; };
@@ -77,10 +79,11 @@ public sealed partial class RustService
"select files", "select files",
async () => async () =>
{ {
var startPath = initialFile is null ? null : PathTools.FindNearestExistingPath(initialFile);
var payload = new SelectFileOptions var payload = new SelectFileOptions
{ {
Title = title, Title = title,
PreviousFile = initialFile is null ? null : new (initialFile), PreviousFile = startPath is null ? null : new (startPath),
Filter = FileTypes.ToFileDialogFilter(filter) Filter = FileTypes.ToFileDialogFilter(filter)
}; };
@@ -26,4 +26,34 @@ public sealed partial class RustService
return await response.Content.ReadFromJsonAsync<ImagePrepareResponse>(this.jsonRustSerializerOptions, token) return await response.Content.ReadFromJsonAsync<ImagePrepareResponse>(this.jsonRustSerializerOptions, token)
?? throw new InvalidDataException("The Rust image preparation returned an empty response."); ?? throw new InvalidDataException("The Rust image preparation returned an empty response.");
} }
/// <summary>
/// Prepares a local image for an AI model in the Rust runtime.
/// </summary>
/// <remarks>
/// The runtime turns the image upright, scales it down, and encodes it anew as JPEG or PNG,
/// within the given number of bytes. An image it cannot prepare, e.g., in an unsupported format,
/// comes back with an error code. So does a failed call to the runtime, as UNKNOWN, and never as
/// an empty image.
/// </remarks>
/// <param name="path">The absolute path of the image file.</param>
/// <param name="maxBytes">The largest number of bytes the encoded image may have, before base64.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The prepared image, or why it could not be prepared.</returns>
public async Task<PreparedModelImage> PrepareImageForModelAsync(string path, long maxBytes, CancellationToken token = default)
{
try
{
using var response = await this.http.PostAsJsonAsync("/image/prepare-for-model", new { path, maxBytes }, this.jsonRustSerializerOptions, token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PreparedModelImage>(this.jsonRustSerializerOptions, token)
?? PreparedModelImage.Failed(ModelImageErrorCode.UNKNOWN);
}
catch (Exception e) when (e is not OperationCanceledException)
{
this.logger?.LogError(e, "Preparing the image '{ImagePath}' for an AI model failed in the runtime.", path);
return PreparedModelImage.Failed(ModelImageErrorCode.UNKNOWN);
}
}
} }
@@ -69,6 +69,17 @@ public static class FileExtensionValidation
TB("Images are not supported at this place"))); TB("Images are not supported at this place")));
return false; return false;
//
// Whether AI Studio can prepare an image for a model does not depend on the
// provider. This holds for templates as well, therefore: a HEIC image attached
// there could never be sent.
//
case UseCase.ATTACHING_CONTENT when !FileTypes.IsAllowedPath(filePath, FileTypes.MODEL_IMAGE):
await MessageBus.INSTANCE.SendWarning(new(
Icons.Material.Filled.ImageNotSupported,
string.Format(TB("AI Studio cannot give {0} images to an AI model. Please convert the image to JPG or PNG."), Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant())));
return false;
// In this use case, we don't validate the provider capabilities: // In this use case, we don't validate the provider capabilities:
case UseCase.ATTACHING_CONTENT when !validateMediaFileTypes: case UseCase.ATTACHING_CONTENT when !validateMediaFileTypes:
return true; return true;
@@ -116,31 +127,4 @@ public static class FileExtensionValidation
TB("Unsupported file type"))); TB("Unsupported file type")));
return false; return false;
} }
/// <summary>
/// Validates that the file is a supported image format and sends appropriate MessageBus notifications when invalid.
/// </summary>
/// <param name="filePath">The file path to validate.</param>
/// <returns>True if valid image, false if invalid (error already sent via MessageBus).</returns>
public static async Task<bool> IsImageExtensionValidWithNotifyAsync(string filePath)
{
var ext = Path.GetExtension(filePath).TrimStart('.');
if (string.IsNullOrWhiteSpace(ext))
{
await MessageBus.INSTANCE.SendError(new(
Icons.Material.Filled.ImageNotSupported,
TB("File has no extension")));
return false;
}
if (FileTypes.IsAllowedPath(filePath, FileTypes.IMAGE))
{
await MessageBus.INSTANCE.SendError(new(
Icons.Material.Filled.ImageNotSupported,
TB("Unsupported image format")));
return false;
}
return true;
}
} }
@@ -737,6 +737,36 @@ public static partial class WorkspaceBehaviour
} }
} }
/// <summary>
/// Opens or creates the workspace a chat template keeps its chats in, by its name.
/// </summary>
/// <remarks>
/// Both the chat page and assistant chat launchers come here, so that a chat finds the workspace
/// of its template the same way wherever it starts. Unlike a workspace a launcher names itself,
/// a failure stops nothing: like everything else a chat template names, a workspace which cannot
/// be used leaves the chat to start as it would without the template, i.e., without a workspace.
/// The failure is logged here, once, so the callers need no handling of their own.
/// </remarks>
/// <param name="workspaceName">The name of the workspace, as the chat template names it.</param>
/// <param name="chatTemplateName">The name of the chat template, for the log.</param>
/// <returns>The ID of the workspace, or Guid.Empty when it can be neither opened nor created.</returns>
public static async Task<Guid> ResolveOrCreateChatTemplateWorkspaceAsync(string workspaceName, string chatTemplateName)
{
try
{
var workspaceId = await ResolveOrCreateWorkspaceIdByNameAsync(workspaceName);
if (workspaceId == Guid.Empty)
LOG.LogWarning("The workspace '{WorkspaceName}' of the chat template '{ChatTemplateName}' could not be opened or created. The chat starts without a workspace.", workspaceName, chatTemplateName);
return workspaceId;
}
catch (Exception exception)
{
LOG.LogError(exception, "The workspace '{WorkspaceName}' of the chat template '{ChatTemplateName}' could not be opened or created. The chat starts without a workspace.", workspaceName, chatTemplateName);
return Guid.Empty;
}
}
public static async Task<TryCreateWorkspaceResult> TryCreateWorkspaceAsync(string workspaceName) public static async Task<TryCreateWorkspaceResult> TryCreateWorkspaceAsync(string workspaceName)
{ {
var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName); var normalizedWorkspaceName = NormalizeWorkspaceName(workspaceName);
@@ -24,6 +24,10 @@
- Added a choice of how AI Studio encrypts API keys, tokens, and passwords when you export providers, data sources, or tool settings for your organization. The new protected format needs AI Studio v26.10.1 or newer on every computer that reads it, and your encryption secret stays the same. If you used AI Studio before, your exports keep the previous format until you switch, so your organization can update all computers first. You find the choice in the app settings, under Enterprise Administration. - Added a choice of how AI Studio encrypts API keys, tokens, and passwords when you export providers, data sources, or tool settings for your organization. The new protected format needs AI Studio v26.10.1 or newer on every computer that reads it, and your encryption secret stays the same. If you used AI Studio before, your exports keep the previous format until you switch, so your organization can update all computers first. You find the choice in the app settings, under Enterprise Administration.
- Added organization-wide management for the format in which exports encrypt API keys, tokens, and passwords. IT departments can set it for all their administrators, either fixed or as a default which users may change. - Added organization-wide management for the format in which exports encrypt API keys, tokens, and passwords. IT departments can set it for all their administrators, either fixed or as a default which users may change.
- Added a way for IT departments to ask you for your personal API key on the start page. When the configuration of your organization brings providers that need your own key, the start page explains how to get it, checks the key with the provider, and stores it for all of these providers at once. Thanks, Dominic Neuburg (`donework`), for this contribution. - Added a way for IT departments to ask you for your personal API key on the start page. When the configuration of your organization brings providers that need your own key, the start page explains how to get it, checks the key with the provider, and stores it for all of these providers at once. Thanks, Dominic Neuburg (`donework`), for this contribution.
- Added providers, profiles, and workspaces to your chat templates. A template can now decide which provider, and with it which model, and which profile a chat starts with, and keep its chats in a workspace of its own. A template for your mails, for example, can start every chat with a provider that meets the confidence level your mailbox requires. You can still pick another provider or profile in the chat.
- Added a way for IT departments to roll out chat templates that bring their own provider, profile, and workspace. Set the template up in the app, then export it as ready-made Lua code for your configuration plugin.
- Added settings to the Assistant Builder. There you choose the provider, and with it the model, that the Assistant Builder starts with, the language in which the assistants you build answer, and the minimum confidence level a provider needs. Thanks, Nils Kruthoff (`nilskruthoff`), for this contribution.
- Added organization-wide management for the defaults of the Assistant Builder. IT departments can preset its provider, the language in which the assistants built with it answer, and the minimum confidence level, either fixed or as a default which users may change.
- Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it. - Improved the Read Web Page tool. When you have not allowed the AI to choose web addresses freely, the tool now refuses every address that does not appear in your chat or in what the tools returned, instead of only asking the AI not to read it.
- Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal. - Improved the installation on macOS: AI Studio is now signed and checked by Apple, so it opens like any other app. You no longer need the Terminal.
- Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better. - Improved the installation on Windows: AI Studio is now signed, so Windows shows who published it. For a while, Windows may still ask you to confirm the installation until it knows the app better.
@@ -38,8 +42,10 @@
- Improved local data sources for folders. You can now choose which files AI Studio indexes, for example, only PDF files, and whether it includes subfolders as well as hidden files and folders. By default, AI Studio now also indexes text files it did not recognize before, such as program code of any kind or a LICENSE file, so a Git repository makes a useful data source. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution. - Improved local data sources for folders. You can now choose which files AI Studio indexes, for example, only PDF files, and whether it includes subfolders as well as hidden files and folders. By default, AI Studio now also indexes text files it did not recognize before, such as program code of any kind or a LICENSE file, so a Git repository makes a useful data source. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Improved local data sources for folders, which now leave out hidden files and folders as well as build output and downloaded dependencies, such as the folders `node_modules`, `bin`, and `obj`. You can change this in the expert settings of the data source. When an existing data source contains such files, AI Studio removes them from its index during the next update. - Improved local data sources for folders, which now leave out hidden files and folders as well as build output and downloaded dependencies, such as the folders `node_modules`, `bin`, and `obj`. You can change this in the expert settings of the data source. When an existing data source contains such files, AI Studio removes them from its index during the next update.
- Improved the background embeddings page, which now lists the files of a local data source that AI Studio left out, for example, images or files that are too large. It also shows which file types and folders lie outside what the data source selects. - Improved the background embeddings page, which now lists the files of a local data source that AI Studio left out, for example, images or files that are too large. It also shows which file types and folders lie outside what the data source selects.
- Improved the Batch Processing assistant, which now also processes images in JPG, PNG, WebP, GIF, BMP, and TIFF format when the selected model can read images. If you used the assistant before, your saved file patterns stay as they are: click "Restore default patterns" or add patterns such as `*.jpg;*.png` yourself.
- Improved the Batch Processing assistant, which now leaves out hidden files and folders as well as build output and downloaded dependencies, such as the folder `node_modules`. You can change both in the expert settings of the assistant. - Improved the Batch Processing assistant, which now leaves out hidden files and folders as well as build output and downloaded dependencies, such as the folder `node_modules`. You can change both in the expert settings of the assistant.
- Improved local data sources for folders and the Batch Processing assistant, which now leave out files larger than 1 GB, such as images of virtual machines. You can change this limit. - Improved local data sources for folders and the Batch Processing assistant, which now leave out files larger than 1 GB, such as images of virtual machines. You can change this limit.
- Improved local data sources for files and folders. When you move or rename the file or folder, for example, to another drive or a new computer, you can now select its new location when you edit the data source. AI Studio keeps what it has already prepared for every unchanged file, so these files do not go to your embedding provider again. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Improved the organization-wide management of the Batch Processing assistant. IT departments can now preset whether it includes hidden files and folders, which files and folders it leaves out, and how large a file may be, either fixed or as a default which users may change. - Improved the organization-wide management of the Batch Processing assistant. IT departments can now preset whether it includes hidden files and folders, which files and folders it leaves out, and how large a file may be, either fixed or as a default which users may change.
- Improved the organization-wide management of the app settings. IT departments can now also set whether AI Studio saves energy while the AI writes its answers, which helps on virtual desktops, for example. - Improved the organization-wide management of the app settings. IT departments can now also set whether AI Studio saves energy while the AI writes its answers, which helps on virtual desktops, for example.
- Improved attaching files to a chat. You can now attach any file that contains readable text, for example, program code or configuration files of a kind AI Studio did not know before. - Improved attaching files to a chat. You can now attach any file that contains readable text, for example, program code or configuration files of a kind AI Studio did not know before.
@@ -47,6 +53,9 @@
- Improved the dialogs for managing your data sources, profiles, and chat templates, which you open, for example, from a chat. The buttons for adding and importing now stay at the bottom of the dialog, so you no longer need to scroll past a long list to reach them. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution. - Improved the dialogs for managing your data sources, profiles, and chat templates, which you open, for example, from a chat. The buttons for adding and importing now stay at the bottom of the dialog, so you no longer need to scroll past a long list to reach them. Thanks, Paul Koudelka (`PaulKoudelka`), for this contribution.
- Improved how often AI Studio signs in to the server of an external data source (ERI server). AI Studio now keeps the server's answer to its security check for a minute, so a server which limits sign-ins no longer refuses the searches of the AI. - Improved how often AI Studio signs in to the server of an external data source (ERI server). AI Studio now keeps the server's answer to its security check for a minute, so a server which limits sign-ins no longer refuses the searches of the AI.
- Improved the search results from external data sources (ERI servers), which now tell the AI what kind of content a result comes from, for example, a dictionary or a book chapter, when the server names it. - Improved the search results from external data sources (ERI servers), which now tell the AI what kind of content a result comes from, for example, a dictionary or a book chapter, when the server names it.
- Improved images in chats and assistants. AI Studio now scales large photos down before it sends them to the AI, so they no longer fail at providers with size limits, such as Anthropic. Photos also no longer carry their location data along.
- Improved attaching images to chats and assistants. AI Studio now converts BMP and TIFF images into a format the AI understands, and it tells you right away when it cannot give an image to the AI, for example, in HEIC or SVG format.
- Improved the jump to the right page for the sources of your own documents (RAG): when you open PDFs with PdfCraft, the document now opens directly at the page where the passage was found, on Windows, macOS, and Linux. Each source opens in a PdfCraft window of its own.
- Changed the banner on the start page to a fresh design that shows what AI Studio can do today. - Changed the banner on the start page to a fresh design that shows what AI Studio can do today.
- Changed when a chat you add to a workspace in the workspace list appears there. Like any other new chat, it now appears once there is something to keep, for example, your first message, instead of right away. - Changed when a chat you add to a workspace in the workspace list appears there. Like any other new chat, it now appears once there is something to keep, for example, your first message, instead of right away.
- Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short. - Fixed a warning about the confidence level that stayed in the dialog for adding a local data source, even after you chose an embedding provider that meets it. The dialog now checks again whenever you change either of them, so a warning also appears when you switch to a provider that falls short.
@@ -71,7 +80,7 @@
- Fixed AI Studio continuing to update the index of a data source with the old settings after you, or the configuration of your organization, changed its embedding provider or the expert settings of the data source, which could take hours for nothing. AI Studio now stops right away and starts over with the new settings. - Fixed AI Studio continuing to update the index of a data source with the old settings after you, or the configuration of your organization, changed its embedding provider or the expert settings of the data source, which could take hours for nothing. AI Studio now stops right away and starts over with the new settings.
- Fixed AI Studio continuing to send the content of a data source to an embedding provider after you deleted that provider, or after a confidence level changed so that the provider may no longer read the data source. AI Studio now stops right away. - Fixed AI Studio continuing to send the content of a data source to an embedding provider after you deleted that provider, or after a confidence level changed so that the provider may no longer read the data source. AI Studio now stops right away.
- Fixed data sources staying unavailable after the configuration of your organization changed their embedding provider, until one of their documents changed, or you restarted AI Studio. When AI Studio updates your data sources automatically, it now rebuilds their index right away. - Fixed data sources staying unavailable after the configuration of your organization changed their embedding provider, until one of their documents changed, or you restarted AI Studio. When AI Studio updates your data sources automatically, it now rebuilds their index right away.
- Fixed AI Studio deleting the index of a local data source when it could not reach its folder or file, for example, because the drive was not connected. Once the folder was back, AI Studio indexed everything again, which could take hours. AI Studio now keeps the index and tells you that it cannot reach the folder. The same applies to subfolders you are not allowed to open. - Fixed AI Studio deleting the index of a local data source when it could not reach its folder or file, for example, because the drive was not connected. Once the folder was back, AI Studio indexed everything again, which could take hours. AI Studio now keeps the index and tells you that it cannot reach the folder. The same applies to subfolders you are not allowed to open and to a folder that suddenly appears empty, which can happen when a drive is not connected.
- Fixed AI Studio rebuilding the index of a data source from scratch without asking you when part of the index was missing on your computer. Since this sends every document to your embedding provider once more, AI Studio now tells you about the missing index and offers to repair the data source. - Fixed AI Studio rebuilding the index of a data source from scratch without asking you when part of the index was missing on your computer. Since this sends every document to your embedding provider once more, AI Studio now tells you about the missing index and offers to repair the data source.
- Fixed saving a local data source whose embedding provider had been deleted, without choosing a new one. The dialog now asks you to select an embedding provider, so that AI Studio can prepare the data source again. - Fixed saving a local data source whose embedding provider had been deleted, without choosing a new one. The dialog now asks you to select an embedding provider, so that AI Studio can prepare the data source again.
- Fixed justified texts being hyphenated by the rules of English even when AI Studio shows another language, such as German. Screen readers now also know which language AI Studio uses. - Fixed justified texts being hyphenated by the rules of English even when AI Studio shows another language, such as German. Screen readers now also know which language AI Studio uses.
@@ -90,6 +99,8 @@
- Fixed a deleted workspace coming back as "Unnamed workspace" when one of its chats was still open or being answered. AI Studio now deletes a workspace only when none of its chats is being answered or transcribed, and it closes the chat you had open in it. - Fixed a deleted workspace coming back as "Unnamed workspace" when one of its chats was still open or being answered. AI Studio now deletes a workspace only when none of its chats is being answered or transcribed, and it closes the chat you had open in it.
- Fixed a chat you started by attaching an audio or video file getting a German name, regardless of the language you use AI Studio in. - Fixed a chat you started by attaching an audio or video file getting a German name, regardless of the language you use AI Studio in.
- Fixed AI Studio sometimes warning about unsaved changes when you left a chat in which you had only selected a chat template, without writing anything yourself. - Fixed AI Studio sometimes warning about unsaved changes when you left a chat in which you had only selected a chat template, without writing anything yourself.
- Fixed canceling the question about unsaved changes after you picked another chat template: the open chat stayed, but it showed the new template, and your message held the text and attachments of that template. Now everything stays as it was.
- Fixed a tile that opens a chat directly refusing to open because of a missing data source while the RAG preview is switched off. Without RAG, a chat uses no data sources, so the tile now opens anyway, and the Assistant Builder no longer offers data sources for it.
- Fixed regenerating an answer clearing the message you had started to write. - Fixed regenerating an answer clearing the message you had started to write.
- Fixed AI Studio waiting for an answer you had removed while it was still being written, so you could not send another message. While an answer is being written, the messages, attachments, provider, profile, and data sources of the chat now stay as they are until you stop the answer or it is complete. Thanks, Bernhard, for reporting this issue. - Fixed AI Studio waiting for an answer you had removed while it was still being written, so you could not send another message. While an answer is being written, the messages, attachments, provider, profile, and data sources of the chat now stay as they are until you stop the answer or it is complete. Thanks, Bernhard, for reporting this issue.
- Fixed the warning about unsaved changes in a chat appearing only by chance when you save your chats manually or have turned off workspaces. AI Studio now reliably asks before you open another chat, create a new one, or leave the chat page, so you no longer lose your changes by accident. - Fixed the warning about unsaved changes in a chat appearing only by chance when you save your chats manually or have turned off workspaces. AI Studio now reliably asks before you open another chat, create a new one, or leave the chat page, so you no longer lose your changes by accident.
@@ -107,7 +118,11 @@
- Fixed AI Studio not checking the transcripts of audio and video files for suspicious instructions (prompt injection) in the Batch Processing assistant and when you load such a file into a field of an assistant. AI Studio now checks these transcripts like all other documents before the AI sees them, even very long ones. - Fixed AI Studio not checking the transcripts of audio and video files for suspicious instructions (prompt injection) in the Batch Processing assistant and when you load such a file into a field of an assistant. AI Studio now checks these transcripts like all other documents before the AI sees them, even very long ones.
- Fixed AI Studio deleting the API keys you entered yourself for the providers of your organization when your organization changed the secret that protects its configuration. These keys now stay, so you no longer need to enter them again. - Fixed AI Studio deleting the API keys you entered yourself for the providers of your organization when your organization changed the secret that protects its configuration. These keys now stay, so you no longer need to enter them again.
- Fixed sharing plugins when the share dialog of Windows or macOS does not open or stops responding. AI Studio now notices this and offers to save the plugin archive to a location of your choice instead. Until you restart AI Studio, the plugin page then offers this export instead of sharing. - Fixed sharing plugins when the share dialog of Windows or macOS does not open or stops responding. AI Studio now notices this and offers to save the plugin archive to a location of your choice instead. Until you restart AI Studio, the plugin page then offers this export instead of sharing.
- Fixed the Batch Processing assistant finding no documents when you chose an output folder that contains the input folder, such as its parent folder.
- Fixed the Batch Processing assistant processing its own result files again as new documents when you chose the input folder as the output folder.
- Fixed chats failing with an error from the provider when an attached image could not be read. AI Studio now leaves out such an image and tells you why.
- Fixed photos taken with a phone appearing rotated in the Visual Briefing assistant when it optimizes images.
- Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far. - Updated the code contributions on the supporters page, which now thank everyone who has contributed code to AI Studio so far.
- Upgraded several libraries to improve security. - Upgraded several libraries to improve security.
- Upgraded to Rust v1.99.0 - Upgraded to Rust v1.99.1
- Upgraded to .NET 9.0.21 - Upgraded to .NET 9.0.21
@@ -0,0 +1,165 @@
using AIStudio.Assistants.BatchProcessing;
namespace AIStudio.Tests.Assistants;
/// <summary>
/// Checks which files the search of the batch processing assistant leaves out because a batch run
/// wrote them.
/// </summary>
/// <remarks>
/// A run writes everything directly into its output folder. The search comes across any of it only
/// when that folder lies below the input folder or is the input folder itself. An output folder
/// above the input folder once made the search leave out every document.
/// </remarks>
[TestFixture]
public sealed class BatchProcessingOutputFolderTests
{
private const string RESULTS_FILE_NAME = "batch-results.csv";
private static readonly string DESKTOP = Path.Combine(Path.GetTempPath(), "ai-studio-batch-output-folder", "Desktop");
private static readonly string PROJECT_FOLDER = Path.Combine(DESKTOP, "Batch");
private static readonly string INPUT_FOLDER = Path.Combine(PROJECT_FOLDER, "Documents");
[Test]
public void AnOutputFolderAboveTheInputFolderLeavesOutNoDocument()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, PROJECT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "notes", "minutes.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.False, "The log of a run lies in the output folder, so a file of this name in the input folder is a document.");
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
[Test]
public void AnOutputFolderSeveralLevelsAboveTheInputFolderLeavesOutNoDocument()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, DESKTOP, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "notes", "minutes.md")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
[Test]
public void TheDefaultOutputFolderBelowTheInputFolderIsLeftOut()
{
var resultsFolder = Path.Combine(INPUT_FOLDER, "ai-results");
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, resultsFolder, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(resultsFolder, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(resultsFolder), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
[Test]
public void AnOutputFolderDeepBelowTheInputFolderIsLeftOutAlone()
{
var parentFolder = Path.Combine(INPUT_FOLDER, "2026", "october");
var resultsFolder = Path.Combine(parentFolder, "results");
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, resultsFolder, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(resultsFolder, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(parentFolder, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "2026")), Is.True);
Assert.That(outputFolder.Enters(parentFolder), Is.True);
Assert.That(outputFolder.Enters(resultsFolder), Is.False);
});
}
[Test]
public void ADocumentWhoseNameBeginsLikeTheOutputFolderIsNoOutput()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, Path.Combine(INPUT_FOLDER, "out"), RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "output-notes.md")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "output")), Is.True);
});
}
[Test]
public void AnOutputFolderNextToTheInputFolderWithTheSameBeginningLeavesOutNothing()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, $"{INPUT_FOLDER}-results", RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True);
});
}
[TestCase(false)]
[TestCase(true)]
public void TheInputFolderAsOutputFolderLeavesOutTheLogAndTheResultsTable(bool withTrailingSeparator)
{
var outputDirectory = withTrailingSeparator ? INPUT_FOLDER + Path.DirectorySeparatorChar : INPUT_FOLDER;
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, outputDirectory, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "log.csv")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, RESULTS_FILE_NAME)), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.pdf")), Is.False);
Assert.That(outputFolder.Enters(Path.Combine(INPUT_FOLDER, "notes")), Is.True, "Leaving out the output folder would leave out the input folder, too.");
});
}
[Test]
public void TheInputFolderAsOutputFolderLeavesOutTheResultFilesOfEveryFormat()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result.md")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_2.docx")), Is.True, "A second result of the same name gets a counter.");
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "Report_Result.HTML")), Is.True);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.v2_result.md")), Is.True, "Only the last extension of the document makes way for the suffix.");
});
}
[Test]
public void TheInputFolderAsOutputFolderKeepsDocumentsWithSimilarNames()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "result.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "my_results.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_draft.md")), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(INPUT_FOLDER, "report_result_.md")), Is.False);
});
}
[Test]
public void TheInputFolderAsOutputFolderKeepsFilesInSubfolders()
{
var outputFolder = new BatchProcessingOutputFolder(INPUT_FOLDER, INPUT_FOLDER, RESULTS_FILE_NAME);
var subfolder = Path.Combine(INPUT_FOLDER, "notes");
Assert.Multiple(() =>
{
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, "log.csv")), Is.False, "A run writes into the output folder itself, never into one of its subfolders.");
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, RESULTS_FILE_NAME)), Is.False);
Assert.That(outputFolder.IsOutputOfRun(Path.Combine(subfolder, "report_result.md")), Is.False);
});
}
}
@@ -1,6 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Provider.OpenAI; using AIStudio.Provider.OpenAI;
using AIStudio.Tests.Tools;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.Extensions.DependencyInjection;
namespace AIStudio.Tests.Chat; namespace AIStudio.Tests.Chat;
@@ -13,10 +18,47 @@ namespace AIStudio.Tests.Chat;
/// Somebody who switched image input on for their own installation watched the picture attach and /// Somebody who switched image input on for their own installation watched the picture attach and
/// then watched it disappear on the way to the model -- every chat round and every tool round, with /// then watched it disappear on the way to the model -- every chat round and every tool round, with
/// nothing anywhere saying why. /// nothing anywhere saying why.
///
/// Each picture is prepared for the model on its way, by the runtime, which a test plays here.
/// A picture which cannot be prepared has to stay out of the message entirely.
/// </remarks> /// </remarks>
[TestFixture] [TestFixture]
public sealed class ListContentBlockExtensionsTests public sealed class ListContentBlockExtensionsTests
{ {
private string picturePath = string.Empty;
private FakeImageRuntime runtime = null!;
private RustService rustService = null!;
private ServiceProvider serviceProvider = null!;
private IServiceProvider previousServiceProvider = null!;
[SetUp]
public void PrepareServices()
{
this.picturePath = Path.Combine(Path.GetTempPath(), $"picture-{Guid.NewGuid():N}.webp");
File.WriteAllBytes(this.picturePath, [1, 2, 3]);
this.runtime = new FakeImageRuntime();
this.rustService = new RustService(this.runtime);
this.previousServiceProvider = Program.SERVICE_PROVIDER;
this.serviceProvider = new ServiceCollection()
.AddSingleton(this.rustService)
.AddSingleton<ModelImageService>()
.AddLogging()
.BuildServiceProvider();
Program.SERVICE_PROVIDER = this.serviceProvider;
}
[TearDown]
public void RestoreApplicationState()
{
Program.SERVICE_PROVIDER = this.previousServiceProvider;
this.serviceProvider.Dispose();
this.rustService.Dispose();
this.runtime.Dispose();
File.Delete(this.picturePath);
}
[Test] [Test]
public async Task ImageInputSwitchedOnByHandReachesTheMessageAsWell() public async Task ImageInputSwitchedOnByHandReachesTheMessageAsWell()
{ {
@@ -31,6 +73,32 @@ public sealed class ListContentBlockExtensionsTests
Assert.That(messages.Single(), Is.InstanceOf<MultimodalMessage>(), "The picture is part of the message because the person said this model can read one."); Assert.That(messages.Single(), Is.InstanceOf<MultimodalMessage>(), "The picture is part of the message because the person said this model can read one.");
} }
[Test]
public async Task APictureWhichCannotBePreparedLeavesNoEmptyPart()
{
this.runtime.ErrorCode = "NOT_DECODABLE";
var messages = await BlocksWithAPicture(this.picturePath).BuildMessagesUsingNestedImageUrlAsync(PictureReadingProvider());
var message = messages.Single() as MultimodalMessage;
Assert.That(message, Is.Not.Null, "The text still goes to the model.");
Assert.That(message!.Content, Has.Count.EqualTo(1).And.All.InstanceOf<SubContentText>(), "A picture without data makes the provider refuse the whole request.");
}
[Test]
public async Task TheTypeOfAPictureComesFromItsPreparation()
{
// The file is a WebP, which the runtime prepared as JPEG:
var messages = await BlocksWithAPicture(this.picturePath).BuildMessagesUsingDirectImageUrlAsync(PictureReadingProvider());
var picture = (messages.Single() as MultimodalMessage)!.Content.OfType<SubContentImageUrl>().Single();
Assert.Multiple(() =>
{
Assert.That(picture.ImageUrl, Is.EqualTo($"data:image/jpeg;base64,{FakeImageRuntime.IMAGE_BASE64}"));
Assert.That(this.runtime.RequestedPaths, Is.EqualTo(new[] { new FileInfo(this.picturePath).FullName }));
});
}
[Test] [Test]
public async Task WithoutThatSwitchThePictureStaysOut() public async Task WithoutThatSwitchThePictureStaysOut()
{ {
@@ -41,11 +109,20 @@ public sealed class ListContentBlockExtensionsTests
Assert.That(messages.Single(), Is.InstanceOf<TextMessage>(), "Nothing says this model reads pictures, so the text goes on its own."); Assert.That(messages.Single(), Is.InstanceOf<TextMessage>(), "Nothing says this model reads pictures, so the text goes on its own.");
} }
/// <summary>
/// A model which reads text only, according to the rules, but which a person said reads pictures.
/// </summary>
private static AIStudio.Settings.Provider PictureReadingProvider() => new(0, "test", "Test", LLMProviders.SELF_HOSTED, new Model("llama3.3:70b", null))
{
CapabilityOverrides = new() { MultipleImageInput = true },
};
/// <summary> /// <summary>
/// One block of text with a picture hanging on it. /// One block of text with a picture hanging on it.
/// </summary> /// </summary>
/// <param name="picturePath">Where the picture lies.</param>
/// <returns>The blocks.</returns> /// <returns>The blocks.</returns>
private static List<ContentBlock> BlocksWithAPicture() => private static List<ContentBlock> BlocksWithAPicture(string picturePath = "/tmp/picture.png") =>
[ [
new() new()
{ {
@@ -54,12 +131,12 @@ public sealed class ListContentBlockExtensionsTests
Content = new ContentText Content = new ContentText
{ {
Text = "What is in this picture?", Text = "What is in this picture?",
FileAttachments = [new FileAttachmentImage("picture.png", "/tmp/picture.png", 1_024)], FileAttachments = [new FileAttachmentImage(Path.GetFileName(picturePath), picturePath, 1_024)],
}, },
}, },
]; ];
private static ISubContent Text(string text) => new SubContentText { Text = text }; private static ISubContent Text(string text) => new SubContentText { Text = text };
private static Task<ISubContent> Picture(FileAttachmentImage image) => Task.FromResult<ISubContent>(new SubContentText { Text = image.FileName }); private static ISubContent Picture(PreparedModelImage image) => new SubContentText { Text = image.MimeType };
} }
+70
View File
@@ -0,0 +1,70 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Tests.Provider;
/// <summary>
/// Checks how large an image may be when we send it through a provider.
/// </summary>
/// <remarks>
/// A provider or a self-hosted engine states its limit or says nothing. Either way, we never send
/// more than our own limit, so a larger limit of a provider must never reach the image preparation.
/// </remarks>
[TestFixture]
public sealed class ImageSizeLimitTests
{
[Test]
public void AnthropicLeavesRoomForTheBase64OfTheImage()
{
Assert.Multiple(() =>
{
Assert.That(LLMProviders.ANTHROPIC.GetMaxImageSizeBytes(), Is.EqualTo(3_932_160));
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(ProviderOf(LLMProviders.ANTHROPIC)), Is.EqualTo(3_932_160));
});
}
[Test]
public void AProviderWithoutAStatedLimitGetsOurOwn()
{
Assert.Multiple(() =>
{
Assert.That(LLMProviders.OPEN_AI.GetMaxImageSizeBytes(), Is.Null, "Nobody stated a limit, and no number stands in for that.");
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(ProviderOf(LLMProviders.OPEN_AI)), Is.EqualTo(IImageSourceExtensions.MAX_IMAGE_SIZE_BYTES));
});
}
[Test]
public void LlmmanLeavesRoomForTheTextOfTheConversation()
{
Assert.Multiple(() =>
{
Assert.That(Host.LLMMAN.GetMaxImageSizeBytes(), Is.EqualTo(1_048_576));
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(ProviderOf(LLMProviders.SELF_HOSTED, Host.LLMMAN)), Is.EqualTo(1_048_576));
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(ProviderOf(LLMProviders.SELF_HOSTED, Host.OLLAMA)), Is.EqualTo(IImageSourceExtensions.MAX_IMAGE_SIZE_BYTES), "Ollama states no limit we know of.");
});
}
[Test]
public void AnEngineCountsOnlyForSelfHostedProviders()
{
// A provider keeps the host it was once configured with, even after somebody switched it
// to a cloud provider in the dialog:
var cloudProviderWithAnOldHost = ProviderOf(LLMProviders.OPEN_AI, Host.LLMMAN);
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(cloudProviderWithAnOldHost), Is.EqualTo(IImageSourceExtensions.MAX_IMAGE_SIZE_BYTES));
}
[Test]
public void NoProviderGetsMoreThanOurOwnLimit()
{
Assert.Multiple(() =>
{
foreach (var llmProvider in Enum.GetValues<LLMProviders>())
foreach (var host in Enum.GetValues<Host>())
Assert.That(IImageSourceExtensions.MaxImageSizeBytesFor(ProviderOf(llmProvider, host)), Is.LessThanOrEqualTo(IImageSourceExtensions.MAX_IMAGE_SIZE_BYTES), $"{llmProvider} with {host}");
});
}
private static AIStudio.Settings.Provider ProviderOf(LLMProviders llmProvider, Host host = Host.NONE) => new(0, "test", "Test", llmProvider, new AIStudio.Provider.Model("model", null), Host: host);
}
@@ -7,8 +7,9 @@ using Lua.Standard;
namespace AIStudio.Tests.Settings; namespace AIStudio.Tests.Settings;
/// <summary> /// <summary>
/// Checks the tools and data sources a chat template carries, across the two surfaces which have /// Checks the profile, provider, workspace, tools, and data sources a chat template carries, across
/// to agree on them: the Lua a configuration plugin states, and the Lua the app exports. /// the two surfaces which have to agree on them: the Lua a configuration plugin states, and the Lua
/// the app exports.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The interesting part is not that a value survives, but that the difference between "this /// The interesting part is not that a value survives, but that the difference between "this
@@ -88,6 +89,9 @@ public sealed class ChatTemplateConfigurationTests
{ {
Assert.That(luaCode, Does.Not.Contain("ToolIds")); Assert.That(luaCode, Does.Not.Contain("ToolIds"));
Assert.That(luaCode, Does.Not.Contain("DataSourceOptions")); Assert.That(luaCode, Does.Not.Contain("DataSourceOptions"));
Assert.That(luaCode, Does.Not.Contain("ProviderId"));
Assert.That(luaCode, Does.Not.Contain("ProfileId"));
Assert.That(luaCode, Does.Not.Contain("WorkspaceName"));
}); });
var read = await ParseAsync(luaCode); var read = await ParseAsync(luaCode);
@@ -189,6 +193,140 @@ public sealed class ChatTemplateConfigurationTests
}); });
} }
[Test]
public async Task APreselectedProviderAndProfileSurviveTheExport()
{
var written = NewTemplate() with
{
ProviderId = "44444444-4444-4444-4444-444444444444",
ProfileId = "55555555-5555-5555-5555-555555555555",
};
var read = await ExportAndReadBackAsync(written);
Assert.Multiple(() =>
{
Assert.That(read.ProviderId, Is.EqualTo(written.ProviderId));
Assert.That(read.ProfileId, Is.EqualTo(written.ProfileId));
});
}
[Test]
public async Task StartingWithoutAProfileIsAStatementOfItsOwn()
{
var read = await ExportAndReadBackAsync(NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id });
var silent = await ExportAndReadBackAsync(NewTemplate());
Assert.Multiple(() =>
{
Assert.That(read.ProfileId, Is.EqualTo(Profile.NO_PROFILE.Id), "Reading this back as null would hand out the default profile the template ruled out.");
Assert.That(silent.ProfileId, Is.Null);
Assert.That(silent.ProviderId, Is.Null);
});
}
[Test]
public async Task APluginTemplateMayNameTheProviderAndProfileOfItsPlugin()
{
var read = await ParseAsync("""
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "33333333-3333-3333-3333-333333333333",
["Name"] = "Mail",
["SystemPrompt"] = "You help with mails.",
["AllowProfileUsage"] = true,
["ProviderId"] = "44444444-4444-4444-4444-444444444444",
["ProfileId"] = "55555555-5555-5555-5555-555555555555",
}
""");
Assert.Multiple(() =>
{
Assert.That(read.ProviderId, Is.EqualTo("44444444-4444-4444-4444-444444444444"));
Assert.That(read.ProfileId, Is.EqualTo("55555555-5555-5555-5555-555555555555"));
});
}
[TestCase("\"\"")]
[TestCase("\"no GUID\"")]
[TestCase("42")]
[TestCase("\"00000000-0000-0000-0000-000000000000\"")]
public async Task AProviderIdWhichNamesNoProviderIsDropped(string luaValue)
{
var read = await ParseAsync($$"""
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "33333333-3333-3333-3333-333333333333",
["Name"] = "Mail",
["SystemPrompt"] = "You help with mails.",
["ProviderId"] = {{luaValue}},
}
""");
Assert.That(read.ProviderId, Is.Null, "The template still works then, with the provider its chats would start with anyway.");
}
[Test]
public async Task AWorkspaceNameSurvivesTheExport()
{
var written = NewTemplate() with { WorkspaceName = "Mails \"inbox\"" };
var read = await ExportAndReadBackAsync(written);
var silent = await ExportAndReadBackAsync(NewTemplate());
Assert.Multiple(() =>
{
Assert.That(read.WorkspaceName, Is.EqualTo(written.WorkspaceName));
Assert.That(silent.WorkspaceName, Is.Null, "A template without a workspace must keep leaving the chat where it would go anyway.");
});
}
[TestCase("\"\"")]
[TestCase("\" \"")]
[TestCase("42")]
[TestCase("{}")]
public async Task AWorkspaceNameWhichNamesNothingIsDropped(string luaValue)
{
var read = await ParseAsync($$"""
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "33333333-3333-3333-3333-333333333333",
["Name"] = "Mail",
["SystemPrompt"] = "You help with mails.",
["WorkspaceName"] = {{luaValue}},
}
""");
Assert.That(read.WorkspaceName, Is.Null);
}
[Test]
public async Task APluginTemplateNamesItsWorkspaceWithoutTheSpacesAround()
{
var read = await ParseAsync("""
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
["Id"] = "33333333-3333-3333-3333-333333333333",
["Name"] = "Mail",
["SystemPrompt"] = "You help with mails.",
["WorkspaceName"] = " Mails ",
}
""");
Assert.That(read.WorkspaceName, Is.EqualTo("Mails"), "Workspaces are matched by their trimmed name, so the spaces would only make the field look different from the name in the list.");
}
[Test]
public void ExportedProviderAndProfileIdsComeWithTheNoteThatTheyAreLocalOnes()
{
var withProvider = NewTemplate() with { ProviderId = "44444444-4444-4444-4444-444444444444" };
var withoutProfile = NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id };
Assert.That(withProvider.TryExportAsConfigurationSection(out var withProviderLua, out var issue), Is.True, issue);
Assert.That(withoutProfile.TryExportAsConfigurationSection(out var withoutProfileLua, out issue), Is.True, issue);
Assert.Multiple(() =>
{
Assert.That(withProviderLua, Does.StartWith("--"), "A provider of this machine is mostly the user's own, which no configuration plugin knows.");
Assert.That(withoutProfileLua, Does.Not.StartWith("--"), "Starting without a profile names nothing which could be missing.");
});
}
[Test] [Test]
public void ExportedDataSourceIdsComeWithTheNoteThatTheyAreLocalOnes() public void ExportedDataSourceIdsComeWithTheNoteThatTheyAreLocalOnes()
{ {
@@ -0,0 +1,154 @@
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tests.Tools.ToolCalling;
using AIStudio.Tools.Services;
using Microsoft.Extensions.Logging.Abstractions;
namespace AIStudio.Tests.Settings;
/// <summary>
/// Checks who decides the provider and the profile when an assistant chat launcher and the chat
/// template it opens its chat with both name one.
/// </summary>
/// <remarks>
/// The rule is the one for the tools and the data sources: the chat template wins. Whatever the
/// template names and cannot be used must not stop the launch, though, because the author of the
/// launcher could not fix it in the launcher. The launcher then decides as if the template named none.
/// The last tests cover the data sources, which only stop a launch while the chat can use them.
/// </remarks>
[TestFixture]
[NonParallelizable]
public sealed class ChatTemplateLauncherSelectionTests : ToolRegistryTestBase
{
private const string PLUGIN_NAME = "Mail assistant";
private const string TEMPLATE_PROFILE_ID = "55555555-5555-5555-5555-555555555555";
private static readonly Guid LAUNCHER_PROVIDER_ID = Guid.Parse("11111111-1111-1111-1111-111111111111");
private static readonly Guid LAUNCHER_PROFILE_ID = Guid.Parse("22222222-2222-2222-2222-222222222222");
[Test]
public void TheTemplateWinsOverTheLauncher()
{
var templateProvider = ToolCapableProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Providers.Add(LauncherProvider());
var templateProfile = this.AddProfile(TEMPLATE_PROFILE_ID);
this.AddProfile(LAUNCHER_PROFILE_ID.ToString());
var template = NewTemplate() with { ProviderId = templateProvider.Id, ProfileId = TEMPLATE_PROFILE_ID };
var service = this.CreateService();
var providerResult = service.ResolveProvider(PLUGIN_NAME, template, LAUNCHER_PROVIDER_ID);
var profileResult = service.ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID);
Assert.Multiple(() =>
{
Assert.That(providerResult.Provider, Is.EqualTo(templateProvider));
Assert.That(providerResult.IsExplicit, Is.True);
Assert.That(profileResult.Profile, Is.EqualTo(templateProfile));
});
}
[Test]
public void WhatTheTemplateNamesAndIsGoneLeavesTheChoiceToTheLauncher()
{
var launcherProvider = LauncherProvider();
this.SettingsManager.ConfigurationData.Providers.Add(launcherProvider);
var launcherProfile = this.AddProfile(LAUNCHER_PROFILE_ID.ToString());
var template = NewTemplate() with { ProviderId = "66666666-6666-6666-6666-666666666666", ProfileId = TEMPLATE_PROFILE_ID };
var service = this.CreateService();
var providerResult = service.ResolveProvider(PLUGIN_NAME, template, LAUNCHER_PROVIDER_ID);
var profileResult = service.ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID);
Assert.Multiple(() =>
{
Assert.That(providerResult.Provider, Is.EqualTo(launcherProvider));
Assert.That(providerResult.ErrorMessage, Is.Empty);
Assert.That(profileResult.Profile, Is.EqualTo(launcherProfile));
Assert.That(profileResult.ErrorMessage, Is.Empty);
});
}
[Test]
public void ATemplateProviderBelowTheConfidenceOfTheChatDoesNotStopTheLaunch()
{
var templateProvider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true;
this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH;
var template = NewTemplate() with { ProviderId = templateProvider.Id };
var providerResult = this.CreateService().ResolveProvider(PLUGIN_NAME, template, null);
Assert.Multiple(() =>
{
Assert.That(providerResult.Provider, Is.Not.EqualTo(templateProvider), "A template must not open the way to a provider the user could not pick by hand.");
Assert.That(providerResult.IsExplicit, Is.False, "The launcher names no provider, so the chat starts with its defaults instead of failing.");
Assert.That(providerResult.ErrorMessage, Is.Empty);
});
}
[Test]
public void ATemplateWhichForbidsProfilesLeavesTheProfileToTheLauncher()
{
this.AddProfile(TEMPLATE_PROFILE_ID);
var launcherProfile = this.AddProfile(LAUNCHER_PROFILE_ID.ToString());
var template = NewTemplate() with { AllowProfileUsage = false, ProfileId = TEMPLATE_PROFILE_ID };
var profileResult = this.CreateService().ResolveProfile(PLUGIN_NAME, template, LAUNCHER_PROFILE_ID);
Assert.That(profileResult.Profile, Is.EqualTo(launcherProfile), "The launch drops the profile afterward for such a template; picking the one of the template here would only hide that.");
}
[Test]
public async Task WithoutRagAMissingDataSourceDoesNotStopTheLaunch()
{
var options = new DataSourceOptions { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] };
var result = await this.CreateService().CheckChosenDataSourcesAsync(ToolCapableProvider(), options, originChatTemplate: null);
Assert.Multiple(() =>
{
Assert.That(result.Options, Is.SameAs(options), "The chat uses no data sources without RAG, so the options travel along unchecked and count once RAG is back.");
Assert.That(result.ErrorMessage, Is.Empty);
});
}
[Test]
public async Task WithRagAMissingDataSourceStillStopsTheLaunch()
{
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Add(PreviewFeatures.PRE_RAG_2024);
var options = new DataSourceOptions { DisableDataSources = false, PreselectedDataSourceIds = ["99999999-9999-9999-9999-999999999999"] };
var result = await this.CreateService().CheckChosenDataSourcesAsync(ToolCapableProvider(), options, originChatTemplate: null);
Assert.Multiple(() =>
{
Assert.That(result.Options, Is.Null, "Opening a launcher is one click, so a missing source has to be said out loud.");
Assert.That(result.ErrorMessage, Is.Not.Empty);
});
}
// Data sources are not resolved by these tests beyond their lookup, so the service needs no data source service:
private DirectChatService CreateService() => new(this.SettingsManager, dataSourceService: null!, toolRegistry: this.CreateRegistry(), NullLogger<DirectChatService>.Instance);
private static AIStudio.Settings.Provider LauncherProvider() => LessTrustedProvider() with { Id = LAUNCHER_PROVIDER_ID.ToString() };
private Profile AddProfile(string profileId)
{
var profile = new Profile((uint)this.SettingsManager.ConfigurationData.Profiles.Count + 1, profileId, $"Profile {profileId[..4]}", "I read a lot of mails.", "Answer briefly.");
this.SettingsManager.ConfigurationData.Profiles.Add(profile);
return profile;
}
private static ChatTemplate NewTemplate() => new()
{
Num = 1,
Id = "33333333-3333-3333-3333-333333333333",
Name = "Mail",
SystemPrompt = "You help with mails.",
PredefinedUserPrompt = string.Empty,
ExampleConversation = [],
FileAttachments = [],
AllowProfileUsage = true,
};
}
@@ -24,10 +24,8 @@ public sealed class ChatTemplatePrecedenceTests
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(toolChoice.ToolIds, Is.Null, "Nobody named any tool, so the chat has to keep using the tools of its own default."); Assert.That(toolChoice, Is.EqualTo(ChatTemplateToolChoice.NONE), "Nobody named any tool, so the chat has to keep using the tools of its own default.");
Assert.That(toolChoice.LauncherChoiceDropped, Is.False); Assert.That(optionsChoice, Is.EqualTo(ChatTemplateDataSourceChoice.NONE), "Nobody named any data source, so the chat has to keep using its own default options.");
Assert.That(optionsChoice.Options, Is.Null, "Nobody named any data source, so the chat has to keep using its own default options.");
Assert.That(optionsChoice.LauncherChoiceDropped, Is.False);
}); });
} }
@@ -0,0 +1,268 @@
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tests.Tools.ToolCalling;
namespace AIStudio.Tests.Settings;
/// <summary>
/// Checks which profile and provider of a chat template a chat actually starts with.
/// </summary>
/// <remarks>
/// A preselection must never get past what the user would be allowed to pick by hand: a profile the
/// template forbids, or a provider below the confidence level of the chat. And it must never stop a
/// chat either: whatever the template names and cannot be used leaves the chat to start as if the
/// template said nothing.
/// </remarks>
[TestFixture]
[NonParallelizable]
public sealed class ChatTemplateStartSelectionTests : ToolRegistryTestBase
{
private const string PROFILE_ID = "55555555-5555-5555-5555-555555555555";
private const string DEFAULT_PROFILE_ID = "77777777-7777-7777-7777-777777777777";
private const string OTHER_TEMPLATE_ID = "88888888-8888-8888-8888-888888888888";
[Test]
public void ATemplateWhichSaysNothingLeavesBothInPlace()
{
this.AddProfile();
this.SettingsManager.ConfigurationData.Providers.Add(ToolCapableProvider());
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.GetChatTemplateProfile(NewTemplate()), Is.Null);
Assert.That(this.SettingsManager.GetChatTemplateProvider(NewTemplate()), Is.EqualTo(AIStudio.Settings.Provider.NONE));
});
}
[Test]
public void ATemplateHandsOutWhatItNames()
{
var profile = this.AddProfile();
var provider = ToolCapableProvider();
this.SettingsManager.ConfigurationData.Providers.Add(provider);
var template = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = provider.Id };
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.EqualTo(profile));
Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(provider));
});
}
[Test]
public void StartingWithoutAProfileIsAnAnswerOfItsOwn()
{
var template = NewTemplate() with { ProfileId = Profile.NO_PROFILE.Id };
Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.EqualTo(Profile.NO_PROFILE), "Null would keep the default profile, which is exactly what this template rules out.");
}
[Test]
public void WhatIsGoneLeavesTheChatAsIfTheTemplateSaidNothing()
{
var template = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = "66666666-6666-6666-6666-666666666666" };
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.Null);
Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(AIStudio.Settings.Provider.NONE));
});
}
[Test]
public void AChatWhoseTemplateProfileIsGoneStartsWithTheDefaultProfile()
{
var template = NewTemplate() with { ProfileId = PROFILE_ID };
var withoutDefault = this.SettingsManager.GetChatTemplateStartProfile(template);
var defaultProfile = new Profile(2, "77777777-7777-7777-7777-777777777777", "Default", "I am the default.", string.Empty);
this.SettingsManager.ConfigurationData.Profiles.Add(defaultProfile);
this.SettingsManager.ConfigurationData.Chat.PreselectOptions = true;
this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = defaultProfile.Id;
var withDefault = this.SettingsManager.GetChatTemplateStartProfile(template);
Assert.Multiple(() =>
{
Assert.That(withoutDefault, Is.EqualTo(Profile.NO_PROFILE), "Keeping the profile the chat had would hand out one the previous chat happened to use, a choice nobody made for this one.");
Assert.That(withDefault, Is.EqualTo(defaultProfile));
});
}
[Test]
public void ATemplateWhichSaysNothingKeepsTheProfileAChatStartsWith()
{
this.AddProfile();
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate()), Is.Null);
Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate() with { AllowProfileUsage = false, ProfileId = PROFILE_ID }), Is.Null);
Assert.That(this.SettingsManager.GetChatTemplateStartProfile(NewTemplate() with { ProfileId = PROFILE_ID })!.Id, Is.EqualTo(PROFILE_ID));
});
}
[Test]
public void ATemplateWhichForbidsProfilesHandsOutNone()
{
this.AddProfile();
var template = NewTemplate() with { AllowProfileUsage = false, ProfileId = PROFILE_ID };
Assert.That(this.SettingsManager.GetChatTemplateProfile(template), Is.Null, "The chat locks the profile selection for such a template, so a profile handed out here could neither be seen nor changed.");
}
[Test]
public void AProviderBelowTheConfidenceOfTheChatIsNotHandedOut()
{
var provider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(provider);
this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true;
this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH;
var template = NewTemplate() with { ProviderId = provider.Id };
Assert.That(this.SettingsManager.GetChatTemplateProvider(template), Is.EqualTo(AIStudio.Settings.Provider.NONE), "A template must not open the way to a provider the user could not pick by hand.");
}
[Test]
public void AProviderIdFollowsTheSameRuleAsTheTemplateWhichNamesIt()
{
var trustedProvider = ToolCapableProvider();
var lessTrustedProvider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(trustedProvider);
this.SettingsManager.ConfigurationData.Providers.Add(lessTrustedProvider);
this.SettingsManager.ConfigurationData.Confidence.EnforceGlobalMinimumConfidence = true;
this.SettingsManager.ConfigurationData.Confidence.GlobalMinimumConfidence = ConfidenceLevel.HIGH;
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.GetChatTemplateProvider(trustedProvider.Id), Is.EqualTo(trustedProvider));
Assert.That(this.SettingsManager.GetChatTemplateProvider(lessTrustedProvider.Id), Is.EqualTo(AIStudio.Settings.Provider.NONE), "The chat template dialog and the import warn about exactly the providers a chat leaves out.");
Assert.That(this.SettingsManager.GetChatTemplateProvider("66666666-6666-6666-6666-666666666666"), Is.EqualTo(AIStudio.Settings.Provider.NONE));
Assert.That(this.SettingsManager.GetChatTemplateProvider((string?)null), Is.EqualTo(AIStudio.Settings.Provider.NONE));
});
}
[Test]
public void ATemplateNamesItsWorkspaceOnlyWhileWorkspacesAreUsed()
{
var template = NewTemplate() with { WorkspaceName = " Mails " };
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior = WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY;
var whileUsed = this.SettingsManager.GetChatTemplateWorkspaceName(template);
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior = WorkspaceStorageBehavior.DISABLE_WORKSPACES;
var whileSwitchedOff = this.SettingsManager.GetChatTemplateWorkspaceName(template);
Assert.Multiple(() =>
{
Assert.That(whileUsed, Is.EqualTo("Mails"));
Assert.That(whileSwitchedOff, Is.Null, "Without workspaces, a template must not bring one back for its chats.");
Assert.That(this.SettingsManager.GetChatTemplateWorkspaceName(NewTemplate()), Is.Null);
});
}
[Test]
public void WhatThePreviousTemplatePreselectedIsNotCarriedOver()
{
var templateProfile = this.AddProfile();
var defaultProfile = this.AddDefaultProfile();
var templateProvider = ToolCapableProvider();
var defaultProvider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Providers.Add(defaultProvider);
this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = defaultProvider.Id;
var previousTemplate = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = templateProvider.Id };
var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID };
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, templateProfile), Is.EqualTo(defaultProfile), "The profile was the choice of another template, not one made for this chat.");
Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, templateProvider), Is.EqualTo(defaultProvider), "The provider was the choice of another template, not one made for this chat.");
});
}
[Test]
public void AChoiceMadeByHandStaysWhenTheTemplateSaysNothing()
{
var handPickedProfile = this.AddProfile();
var defaultProfile = this.AddDefaultProfile();
var templateProvider = ToolCapableProvider();
var handPickedProvider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Providers.Add(handPickedProvider);
var previousTemplate = NewTemplate() with { ProfileId = defaultProfile.Id, ProviderId = templateProvider.Id };
var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID };
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, handPickedProfile), Is.EqualTo(handPickedProfile));
Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, handPickedProvider), Is.EqualTo(handPickedProvider));
Assert.That(this.SettingsManager.ChooseChatTemplateProfile(NewTemplate(), NewTemplate(), handPickedProfile), Is.EqualTo(handPickedProfile), "Without any preselection, nothing changes.");
});
}
[Test]
public void TheNewTemplateDecidesWhenItNamesBoth()
{
var templateProfile = this.AddProfile();
var otherProfile = this.AddDefaultProfile();
var templateProvider = ToolCapableProvider();
var otherProvider = LessTrustedProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Providers.Add(otherProvider);
var previousTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID, ProfileId = otherProfile.Id, ProviderId = otherProvider.Id };
var nextTemplate = NewTemplate() with { ProfileId = PROFILE_ID, ProviderId = templateProvider.Id };
Assert.Multiple(() =>
{
Assert.That(this.SettingsManager.ChooseChatTemplateProfile(nextTemplate, previousTemplate, otherProfile), Is.EqualTo(templateProfile));
Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, otherProvider), Is.EqualTo(templateProvider));
});
}
[Test]
public void WithoutADefaultProviderTheChatKeepsItsProvider()
{
var templateProvider = ToolCapableProvider();
this.SettingsManager.ConfigurationData.Providers.Add(templateProvider);
this.SettingsManager.ConfigurationData.Providers.Add(LessTrustedProvider());
var previousTemplate = NewTemplate() with { ProviderId = templateProvider.Id };
var nextTemplate = NewTemplate() with { Id = OTHER_TEMPLATE_ID };
Assert.That(this.SettingsManager.ChooseChatTemplateProvider(nextTemplate, previousTemplate, templateProvider), Is.EqualTo(templateProvider), "A chat without any provider could not answer at all.");
}
private Profile AddDefaultProfile()
{
var defaultProfile = new Profile(2, DEFAULT_PROFILE_ID, "Default", "I am the default.", string.Empty);
this.SettingsManager.ConfigurationData.Profiles.Add(defaultProfile);
this.SettingsManager.ConfigurationData.Chat.PreselectOptions = true;
this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = defaultProfile.Id;
return defaultProfile;
}
private Profile AddProfile()
{
var profile = new Profile(1, PROFILE_ID, "Mail", "I read a lot of mails.", "Answer briefly.");
this.SettingsManager.ConfigurationData.Profiles.Add(profile);
return profile;
}
private static ChatTemplate NewTemplate() => new()
{
Num = 1,
Id = "33333333-3333-3333-3333-333333333333",
Name = "Mail",
SystemPrompt = "You help with mails.",
PredefinedUserPrompt = string.Empty,
ExampleConversation = [],
FileAttachments = [],
AllowProfileUsage = true,
};
}
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff. Show more