mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-27 02:33:38 +00:00
Deliver each drop to the zone under the cursor
This commit is contained in:
parent
d741a6e06d
commit
14066a93fb
@ -2,7 +2,9 @@
|
||||
@inherits AssistantLowerBase
|
||||
@typeparam TSettings
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
@* Every assistant is a drop area: a file dropped anywhere inside it lands on the assistant's
|
||||
default zone, while a file dropped on one of its specific zones lands there. *@
|
||||
<DropZoneScope Class="inner-scrolling-context">
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2 mr-3" StretchItems="StretchItems.Start">
|
||||
<MudText Typo="Typo.h3">
|
||||
@ -186,4 +188,4 @@
|
||||
</MudStack>
|
||||
</FooterContent>
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
</DropZoneScope>
|
||||
@ -7,7 +7,7 @@
|
||||
@T("Input")
|
||||
</MudText>
|
||||
|
||||
<SelectDirectory Label="@T("Folder containing your documents")" DirectoryDialogTitle="@T("Select the folder containing your documents")" @bind-Directory="@this.inputDirectory" Validation="@this.ValidateInputDirectory" Disabled="@this.isProcessingBatch"/>
|
||||
<SelectDirectory Label="@T("Folder containing your documents")" DirectoryDialogTitle="@T("Select the folder containing your documents")" @bind-Directory="@this.inputDirectory" Validation="@this.ValidateInputDirectory" EnableDragDrop="true" Disabled="@this.isProcessingBatch"/>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-1">
|
||||
<MudTextField T="string" @bind-Text="@this.filePatterns" Validation="@this.ValidateFilePatterns" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("File patterns")" HelperText="@T("Which files should be processed? Separate multiple patterns with a semicolon, e.g., *.pdf;*.docx")" AdornmentIcon="@Icons.Material.Filled.FilterAlt" Adornment="Adornment.Start" Variant="Variant.Outlined" Margin="Margin.Normal" Class="flex-grow-1" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
@ -44,7 +44,7 @@
|
||||
|
||||
@if (this.promptSource is BatchProcessingPromptSource.FREE_PROMPT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
|
||||
<ReadFileContent Text="@T("Load prompt from file")" @bind-FileContent="@this.freePrompt" EnableDragDrop="true" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
|
||||
|
||||
<MudTextField T="string" @bind-Text="@this.freePrompt" Validation="@this.ValidateFreePrompt" Immediate="@true" Disabled="@this.isProcessingBatch" Label="@T("What should the AI do with each document?")" HelperText="@T("These instructions are applied to every single document of the batch run.")" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="5" AutoGrow="@true" MaxLines="26" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
@ -52,7 +52,7 @@
|
||||
}
|
||||
else if (this.promptSource is BatchProcessingPromptSource.FILE_IMPORT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" Filter="@([FileTypes.MARKDOWN])" ShowAttachedDocumentState="@true" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
|
||||
<ReadFileContent Text="@T("Select the file with your instructions")" @bind-FileContent="@this.ImportedPrompt" Filter="@([FileTypes.MARKDOWN])" ShowAttachedDocumentState="@true" EnableDragDrop="true" CatchAllDocuments="true" Disabled="@this.isProcessingBatch"/>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(this.promptFilePath))
|
||||
{
|
||||
@ -161,7 +161,7 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
<SelectDirectory Label="@T("Output folder (optional)")" DirectoryDialogTitle="@T("Select the output folder")" @bind-Directory="@this.outputDirectory" Disabled="@this.isProcessingBatch"/>
|
||||
<SelectDirectory Label="@T("Output folder (optional)")" DirectoryDialogTitle="@T("Select the output folder")" @bind-Directory="@this.outputDirectory" EnableDragDrop="true" Disabled="@this.isProcessingBatch"/>
|
||||
|
||||
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
|
||||
@T("We always write a semicolon-separated log named log.csv, which lists every document with its processing time, the model, the status, and the details of any error. When you start another run on the same output folder, we ask you whether to continue that run: documents which failed or are missing in the log are then processed again. When no output folder is selected, everything is written to the subfolder 'ai-results' within the input folder.")
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
@if (this.step is BuilderStep.DESCRIBE)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load description from file")" @bind-FileContent="@this.assistantDescription" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent Text="@T("Load description from file")" @bind-FileContent="@this.assistantDescription" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" @bind-Text="@this.assistantDescription" Validation="@this.ValidateAssistantDescription" AdornmentIcon="@Icons.Material.Filled.AutoAwesome" Adornment="Adornment.Start" Label="@T("Describe your assistant")" HelperText="@T("Describe the task, inputs, and desired output in your own words. The model will infer all the plugin details.")" Placeholder="@T("I need an assistant that turns meeting notes into clear tasks with owners and deadlines.")" Variant="Variant.Outlined" Lines="8" AutoGrow="@true" MaxLines="18" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
@* This switch chooses between the two kinds of assistant the Builder can create, so it stays
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
@T("You can attach source files as optional context for your coding question.")
|
||||
</MudJustifiedText>
|
||||
<div class="mb-3">
|
||||
<AttachDocuments Name="Coding Source Files" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
<AttachDocuments Name="Coding Source Files" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
</div>
|
||||
|
||||
<MudStack Row="@false" Class="mb-3">
|
||||
|
||||
@ -74,7 +74,7 @@ else
|
||||
@T("Documents for the analysis")
|
||||
</MudText>
|
||||
|
||||
<AttachDocuments Name="Document Analysis Files" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
<AttachDocuments Name="Document Analysis Files" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
@ -166,7 +166,7 @@ else
|
||||
@T("Documents for the analysis")
|
||||
</MudText>
|
||||
|
||||
<AttachDocuments Name="Document Analysis Files" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
<AttachDocuments Name="Document Analysis Files" @bind-DocumentPaths="@this.loadedDocumentPaths" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
|
||||
@ -153,7 +153,7 @@ else
|
||||
{
|
||||
var fileState = this.assistantState.FileContent[fileContent.Name];
|
||||
<div class="@fileContent.Class" style="@GetOptionalStyle(fileContent.Style)">
|
||||
<ReadFileContent @bind-FileContent="@fileState.Content" MediaImportTargetId="@fileContent.Name" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" ShowAttachedDocumentState="@fileContent.ShowAttachedDocumentState" />
|
||||
<ReadFileContent @bind-FileContent="@fileState.Content" MediaImportTargetId="@fileContent.Name" EnableDragDrop="true" CatchAllDocuments="true" ShowAttachedDocumentState="@fileContent.ShowAttachedDocumentState" />
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
@ -169,7 +169,6 @@ else
|
||||
}
|
||||
<div class="px-4">
|
||||
<AttachDocuments Name="@fileAttachment.Name"
|
||||
Layer="@DropLayers.ASSISTANTS"
|
||||
@bind-DocumentPaths="@fileState.DocumentPaths"
|
||||
CatchAllDocuments="@fileAttachment.CatchAllDocuments"
|
||||
UseSmallForm="@fileAttachment.UseSmallForm"
|
||||
|
||||
@ -345,4 +345,4 @@ else
|
||||
</MudJustifiedText>
|
||||
|
||||
<MudTextSwitch Label="@T("Should we write the generated code to the file system?")" Disabled="@this.IsERIInputDisabled" @bind-Value="@this.writeToFilesystem" LabelOn="@T("Yes, please write or update all generated code to the file system")" LabelOff="@T("No, just show me the code")" />
|
||||
<SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@this.IsBaseDirectorySelectionDisabled" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true" />
|
||||
<SelectDirectory Label="@T("Base directory where to write the code")" @bind-Directory="@this.baseDirectory" Disabled="@this.IsBaseDirectorySelectionDisabled" DirectoryDialogTitle="@T("Select the target directory for the ERI server")" Validation="@this.ValidateDirectory" EnableDragDrop="true" CatchAllDocuments="true" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
|
||||
|
||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to check")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
@ -6,7 +6,7 @@
|
||||
<ReadWebContent @bind-Content="@this.inputLegalDocument" ProviderSettings="@this.ProviderSettings" @bind-AgentIsRunning="@this.isAgentRunning" @bind-Preselect="@this.showWebContentReader" @bind-PreselectContentCleanerAgent="@this.useContentCleanerAgent"/>
|
||||
}
|
||||
|
||||
<ReadFileContent @bind-FileContent="@this.inputLegalDocument" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent @bind-FileContent="@this.inputLegalDocument" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputLegalDocument" Validation="@this.ValidatingLegalDocument" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Legal document")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputQuestions" Validation="@this.ValidatingQuestions" AdornmentIcon="@Icons.Material.Filled.QuestionAnswer" Adornment="Adornment.Start" Label="@T("Your questions")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
@ -8,7 +8,7 @@
|
||||
@T("You can enter text, attach one or more documents, or use both. At least one input is required.")
|
||||
</MudJustifiedText>
|
||||
<div class="mb-3">
|
||||
<AttachDocuments Name="My Tasks Documents" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" OnChange="@this.OnDocumentsChanged" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
<AttachDocuments Name="My Tasks Documents" @bind-DocumentPaths="@this.loadedDocumentPaths" OnChange="@this.OnDocumentsChanged" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
</div>
|
||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom target language")" />
|
||||
<ProviderSelection @bind-ProviderSettings="@this.ProviderSettings" ValidateProvider="@this.ValidatingProvider"/>
|
||||
@ -95,7 +95,6 @@
|
||||
@if (this.useCustomPromptGuide)
|
||||
{
|
||||
<AttachDocuments Name="Custom Prompt Guide"
|
||||
Layer="@DropLayers.ASSISTANTS"
|
||||
@bind-DocumentPaths="@this.customPromptGuideFiles"
|
||||
OnChange="@this.OnCustomPromptGuideFilesChanged"
|
||||
CatchAllDocuments="false"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@attribute [Route(Routes.ASSISTANT_REWRITE)]
|
||||
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite>
|
||||
|
||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent Text="@T("Load text from file")" @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input to improve")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Language")" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="@T("Custom language")" />
|
||||
<EnumSelection T="WritingStyles" NameFunc="@(style => style.Name())" @bind-Value="@this.selectedWritingStyle" Icon="@Icons.Material.Filled.Edit" Label="@T("Writing style")" AllowOther="@false" />
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<MudTextField T="string" @bind-Text="@this.inputContent" Validation="@this.ValidatingContext" Adornment="Adornment.Start" Lines="6" MaxLines="12" AutoGrow="@false" Label="@T("Text content")" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-1 mt-1"> @T("Attach documents")</MudText>
|
||||
<AttachDocuments Name="Documents for input" Layer="@DropLayers.ASSISTANTS" @bind-DocumentPaths="@this.loadedDocumentPaths" OnChange="@this.OnDocumentsChanged" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
<AttachDocuments Name="Documents for input" @bind-DocumentPaths="@this.loadedDocumentPaths" OnChange="@this.OnDocumentsChanged" CatchAllDocuments="true" UseSmallForm="false" Provider="@this.ProviderSettings"/>
|
||||
|
||||
<MudText Typo="Typo.h5" Class="mb-3 mt-6"> @T("Details about the desired presentation")</MudText>
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.ProviderSettings" @bind-AgentIsRunning="@this.isAgentRunning" @bind-Preselect="@this.showWebContentReader" @bind-PreselectContentCleanerAgent="@this.useContentCleanerAgent"/>
|
||||
}
|
||||
|
||||
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
<MudTextField T="string" Disabled="@this.isAgentRunning" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Your input")" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="@true" @bind-OtherInput="@this.customTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomLanguage" />
|
||||
<EnumSelection T="Complexity" NameFunc="@(complexity => complexity.Name())" @bind-Value="@this.selectedComplexity" Icon="@Icons.Material.Filled.Layers" Label="@T("Target complexity")" AllowOther="@true" @bind-OtherInput="@this.expertInField" OtherValue="Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS" LabelOther="@T("Your expertise")" ValidateOther="@this.ValidateExpertInField" />
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<ReadWebContent @bind-Content="@this.inputText" ProviderSettings="@this.ProviderSettings" @bind-AgentIsRunning="@this.isAgentRunning" @bind-Preselect="@this.showWebContentReader" @bind-PreselectContentCleanerAgent="@this.useContentCleanerAgent"/>
|
||||
}
|
||||
|
||||
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" Layer="@DropLayers.ASSISTANTS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent @bind-FileContent="@this.inputText" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
|
||||
<MudTextSwitch Label="@T("Live translation")" @bind-Value="@this.liveTranslation" LabelOn="@T("Live translation")" LabelOff="@T("No live translation")"/>
|
||||
@if (this.liveTranslation)
|
||||
|
||||
@ -122,7 +122,6 @@
|
||||
<MudText Typo="Typo.h5">@T("Source material")</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@T("Documents, spreadsheets, images, audio, and video are considered as source context.")</MudText>
|
||||
<AttachDocuments Name="Visual briefing source material"
|
||||
Layer="@DropLayers.ASSISTANTS"
|
||||
@bind-DocumentPaths="@this.editor.SourceMaterial"
|
||||
OnChange="@this.EnforceSourceExclusivityAsync"
|
||||
CatchAllDocuments="true"
|
||||
@ -136,7 +135,6 @@
|
||||
<MudText Typo="Typo.h5">@T("Visual assets")</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-2">@T("PNG, JPEG, and WebP assets are analyzed and must appear visibly in the briefing.")</MudText>
|
||||
<AttachDocuments Name="Visual briefing visual assets"
|
||||
Layer="@DropLayers.ASSISTANTS"
|
||||
@bind-DocumentPaths="@this.editor.VisualAssets"
|
||||
OnChange="@this.EnforceSourceExclusivityAsync"
|
||||
CatchAllDocuments="false"
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
@if (this.UseSmallForm)
|
||||
{
|
||||
<MudStack Spacing="0">
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<div data-drop-zone-id="@this.dropZoneId">
|
||||
@if (this.isDraggingOver)
|
||||
{
|
||||
<MudBadge
|
||||
@ -82,7 +82,7 @@ else
|
||||
{
|
||||
<MediaTranscriptionStatus Owner="@this.EffectiveImportOwner" TargetId="@this.EffectiveMediaImportTarget.TargetId"/>
|
||||
}
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<div data-drop-zone-id="@this.dropZoneId">
|
||||
<MudPaper Height="20em" Outlined="true" Class="@this.dragClass" Style="overflow-y: auto;">
|
||||
@foreach (var fileAttachment in this.DocumentPaths)
|
||||
{
|
||||
|
||||
@ -24,18 +24,6 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
[Parameter]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, pause catching dropped files. Default is false.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool PauseCatchingDrops { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public HashSet<FileAttachment> DocumentPaths { get; set; } = [];
|
||||
|
||||
@ -46,11 +34,23 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
public Func<HashSet<FileAttachment>, Task> OnChange { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Makes this component the default target of its area, meaning of its page, assistant, or
|
||||
/// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
|
||||
/// of their own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only one zone per area can hold that role, and if several ask for it, the first one in the
|
||||
/// markup gets it.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The area this component lives in, if it lives in one at all.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
private DropZoneScopeState? Scope { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool UseSmallForm { get; set; }
|
||||
|
||||
@ -105,8 +105,9 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly string DROP_FILES_HERE_TEXT = TB("Drop files here to attach them.");
|
||||
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private readonly string dropZoneId = $"attach-documents-{Guid.NewGuid():N}";
|
||||
|
||||
private bool isDefaultZone;
|
||||
private bool isDraggingOver;
|
||||
private bool isFileDialogOpen;
|
||||
private MediaImportOwner EffectiveImportOwner => this.OwnerChat is not null
|
||||
@ -122,10 +123,9 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
|
||||
this.ClaimDefaultZoneRole();
|
||||
|
||||
// Register this drop area:
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@ -223,93 +223,37 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||
|
||||
// Release the drop area. Without this, drop areas below this one would count this component
|
||||
// forever and would stop catching dropped files:
|
||||
this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(AttachDocuments)}: releasing the drop area");
|
||||
// Hand the role of the default target back to the area:
|
||||
if (this.isDefaultZone)
|
||||
this.Scope?.ReleaseDefaultZone(this);
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
if (this.IsUnavailable && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
|
||||
return;
|
||||
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer)
|
||||
{
|
||||
this.numDropAreasAboveThis++;
|
||||
this.PauseCatchingDrops = true;
|
||||
}
|
||||
|
||||
case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
|
||||
this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer)
|
||||
{
|
||||
if(this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
case Event.PATHS_DROPPED when data is DroppedPaths dropped:
|
||||
// Whoever the drop was meant for, the drag is over and no zone stays highlighted:
|
||||
this.ApplyHighlight(false);
|
||||
|
||||
if(this.numDropAreasAboveThis is 0)
|
||||
this.PauseCatchingDrops = false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
if (!this.IsThisZone(dropped.ZoneId))
|
||||
return;
|
||||
|
||||
if(!this.isComponentHovered && !this.CatchAllDocuments)
|
||||
if (this.IsUnavailable)
|
||||
{
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop hovered event.", this.Name);
|
||||
this.Logger.LogDebug("The attachment zone '{Name}' is unavailable and swallowed {Count} dropped path(s).", this.Name, dropped.Paths.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDraggingOver = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
this.isDraggingOver = false;
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
this.isDraggingOver = false;
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
|
||||
if(this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
if(!this.isComponentHovered && !this.CatchAllDocuments)
|
||||
{
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is not hovered, ignoring file drop dropped event.", this.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.AddFileBatchAsync(paths);
|
||||
await this.AddFileBatchAsync(dropped.Paths);
|
||||
await this.DocumentPathsChanged.InvokeAsync(this.DocumentPaths);
|
||||
await this.OnChange(this.DocumentPaths);
|
||||
this.isDraggingOver = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
@ -317,6 +261,48 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Asks the area for the role of its default target, if this component wants it.
|
||||
/// </summary>
|
||||
private void ClaimDefaultZoneRole()
|
||||
{
|
||||
if (!this.CatchAllDocuments || this.Scope is null)
|
||||
return;
|
||||
|
||||
this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
|
||||
if (!this.isDefaultZone)
|
||||
this.Logger.LogDebug("The attachment zone '{Name}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether the named zone is this one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The area counts as this zone as long as this zone is its default target. That is the whole
|
||||
/// mechanism behind dropping anywhere in the chat and still landing on the composer.
|
||||
/// </remarks>
|
||||
/// <param name="zoneId">The ID the hit test reported, or null when it hit nothing.</param>
|
||||
private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the zone, or takes the highlight away.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
|
||||
/// second, and without it every one of them would render every zone on the page anew. In the
|
||||
/// small form the highlight also swaps the markup, so this keeps the composer from flickering.
|
||||
/// </remarks>
|
||||
private void ApplyHighlight(bool shouldBeHighlighted)
|
||||
{
|
||||
var highlighted = shouldBeHighlighted && !this.IsUnavailable;
|
||||
if (highlighted == this.isDraggingOver)
|
||||
return;
|
||||
|
||||
this.isDraggingOver = highlighted;
|
||||
this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4" : DEFAULT_DRAG_CLASS;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-4 mt-4 mud-width-full mud-height-full";
|
||||
|
||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||
@ -370,32 +356,6 @@ public partial class AttachDocuments : MSGComponentBase
|
||||
await this.OnChange(this.DocumentPaths);
|
||||
}
|
||||
|
||||
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-4";
|
||||
|
||||
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
|
||||
|
||||
private void OnMouseEnter(EventArgs _)
|
||||
{
|
||||
if(this.IsUnavailable || this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is hovered.", this.Name);
|
||||
this.isComponentHovered = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnMouseLeave(EventArgs _)
|
||||
{
|
||||
if(this.IsUnavailable || this.PauseCatchingDrops)
|
||||
return;
|
||||
|
||||
this.Logger.LogDebug("Attach documents component '{Name}' is no longer hovered.", this.Name);
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task RemoveDocument(FileAttachment fileAttachment)
|
||||
{
|
||||
if (this.IsUnavailable)
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" OwnerChat="@this.ChatThread" EnsureOwnerChatAsync="@this.EnsureMediaImportChatAsync" Disabled="@this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner)"/>
|
||||
<AttachDocuments Name="File Attachments" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" OwnerChat="@this.ChatThread" EnsureOwnerChatAsync="@this.EnsureMediaImportChatAsync" Disabled="@this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner)"/>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
|
||||
8
app/MindWork AI Studio/Components/DropZoneScope.razor
Normal file
8
app/MindWork AI Studio/Components/DropZoneScope.razor
Normal file
@ -0,0 +1,8 @@
|
||||
@* One element, and the content immediately inside it. Layouts hang child selectors on this
|
||||
element, for instance app.css does on .inner-scrolling-context, so an extra level here would
|
||||
break them. The cascading value renders no element of its own. *@
|
||||
<div class="@this.Class" style="@this.Style" data-drop-zone-id="@this.EffectiveState.ScopeId">
|
||||
<CascadingValue Value="@this.EffectiveState" IsFixed="@true">
|
||||
@this.ChildContent
|
||||
</CascadingValue>
|
||||
</div>
|
||||
57
app/MindWork AI Studio/Components/DropZoneScope.razor.cs
Normal file
57
app/MindWork AI Studio/Components/DropZoneScope.razor.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marks an area whose drops end up at its default target when they hit no specific zone.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This is how the habitual behaviour survives the move to hit testing: a file dropped anywhere in
|
||||
/// the chat, in an assistant, or in a dialog still arrives where it used to, while a file dropped
|
||||
/// on a specific zone now arrives exactly there. No code decides between the two -- the browser
|
||||
/// does, because the specific zone lies deeper in the DOM than the area around it, and the hit test
|
||||
/// resolves from the inside out.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The component replaces an existing element rather than adding one: it takes the class and the
|
||||
/// style of the element it stands in for. Where the areas already had a wrapper, which is the case
|
||||
/// for every page and every assistant, nothing about the layout changes.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public partial class DropZoneScope : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The content of the area.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The CSS classes of the element this scope renders.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The inline style of the element this scope renders.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Style { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The state to cascade, for hosts which want to be the default target of their own area.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A host cannot read what it cascades itself, so a page which is its own drop target has to own
|
||||
/// the state instead. The plugins page is such a case: it accepts an archive anywhere on it and
|
||||
/// has no inner zone to hand the role to. Everybody else leaves this alone and lets the scope
|
||||
/// keep its own state.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public DropZoneScopeState? State { get; set; }
|
||||
|
||||
private readonly DropZoneScopeState ownState = new($"drop-zone-scope-{Guid.NewGuid():N}");
|
||||
|
||||
private DropZoneScopeState EffectiveState => this.State ?? this.ownState;
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<div data-drop-zone-id="@this.dropZoneId">
|
||||
<MudPaper Outlined="@true" Class="@this.dragClass">
|
||||
@this.ChildContent
|
||||
</MudPaper>
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
@ -28,22 +26,13 @@ public partial class PathDropZone : MSGComponentBase
|
||||
public EventCallback<List<string>> OnPathsDropped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; } = DropLayers.ROOT;
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Makes this zone the default target of its area, meaning of its page, assistant, or dialog.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Practically every zone needs this today. Hovering is detected through mouse events, and no
|
||||
/// webview delivers those while a native drag is in progress, so a zone without this flag
|
||||
/// hardly ever catches anything. The consequence is that two zones of the same layer cannot be
|
||||
/// told apart: the one carrying this flag takes every drop, including the ones meant for the
|
||||
/// other. A page may therefore hold only one zone per layer. Lifting that limit needs the
|
||||
/// cursor position, which the runtime receives from Tauri and currently discards in
|
||||
/// app_window.rs.
|
||||
/// A drop aimed at this zone arrives here in any case. What this flag decides is the fate of the
|
||||
/// drops aimed anywhere else in the surrounding area which hit no zone of their own: with the
|
||||
/// flag, they arrive here as well. Only one zone per area can hold that role, and if several ask
|
||||
/// for it, the first one in the markup gets it.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
@ -52,127 +41,118 @@ public partial class PathDropZone : MSGComponentBase
|
||||
/// When true, the zone ignores drops and is not highlighted.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The drop area stays registered nevertheless. Releasing it during the lifetime of the
|
||||
/// component would lower the count of every zone below this one, and those zones would then
|
||||
/// catch files while this one is still on screen.
|
||||
/// It keeps its ID in the DOM nevertheless and therefore swallows the drops aimed at it. That is
|
||||
/// what the pointer says: it rests on a switched-off field, so nothing happens. Letting the drop
|
||||
/// fall through to the area behind it would deliver the files somewhere else entirely.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The area this zone lives in, if it lives in one at all.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
private DropZoneScopeState? Scope { get; set; }
|
||||
|
||||
[Inject]
|
||||
private ILogger<PathDropZone> Logger { get; init; } = null!;
|
||||
|
||||
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-3 mb-3 mud-width-full";
|
||||
|
||||
private readonly string dropZoneId = $"path-drop-zone-{Guid.NewGuid():N}";
|
||||
|
||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private bool isDefaultZone;
|
||||
private bool isHighlighted;
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
|
||||
this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
|
||||
this.ClaimDefaultZoneRole();
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the drop area.
|
||||
/// Hands the role of the default target back to the area.
|
||||
/// </summary>
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
// Without this, drop areas below this one would count this component forever and would
|
||||
// stop catching dropped files:
|
||||
this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(PathDropZone)}: releasing the drop area");
|
||||
if (this.isDefaultZone)
|
||||
this.Scope?.ReleaseDefaultZone(this);
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
// A disabled zone takes no files. It keeps track of the zones above it, though, because
|
||||
// those come and go while this one is disabled:
|
||||
if (this.Disabled && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
|
||||
return;
|
||||
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer)
|
||||
case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
|
||||
this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
|
||||
break;
|
||||
|
||||
case Event.PATHS_DROPPED when data is DroppedPaths dropped:
|
||||
// Whoever the drop was meant for, the drag is over and no zone stays highlighted:
|
||||
this.ApplyHighlight(false);
|
||||
|
||||
if (!this.IsThisZone(dropped.ZoneId))
|
||||
return;
|
||||
|
||||
if (this.Disabled)
|
||||
{
|
||||
this.numDropAreasAboveThis++;
|
||||
this.ClearDragClass();
|
||||
this.Logger.LogDebug("The path drop zone '{ZoneId}' is disabled and swallowed {Count} dropped path(s).", this.dropZoneId, dropped.Paths.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer && this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if(!this.CanCatchDroppedPath())
|
||||
return;
|
||||
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
|
||||
if(!this.CanCatchDroppedPath())
|
||||
return;
|
||||
|
||||
this.Logger.LogDebug("The path drop zone on layer {Layer} caught {Count} path(s).", this.Layer, paths.Count);
|
||||
await this.OnPathsDropped.InvokeAsync(paths);
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
this.Logger.LogDebug("The path drop zone '{ZoneId}' caught {Count} path(s).", this.dropZoneId, dropped.Paths.Count);
|
||||
await this.OnPathsDropped.InvokeAsync(dropped.Paths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool CanCatchDroppedPath() => this.numDropAreasAboveThis is 0 && (this.isComponentHovered || this.CatchAllDocuments);
|
||||
|
||||
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2";
|
||||
|
||||
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
|
||||
|
||||
private void OnMouseEnter(EventArgs _)
|
||||
/// <summary>
|
||||
/// Asks the area for the role of its default target, if this zone wants it.
|
||||
/// </summary>
|
||||
private void ClaimDefaultZoneRole()
|
||||
{
|
||||
if(this.Disabled || this.numDropAreasAboveThis > 0)
|
||||
if (!this.CatchAllDocuments || this.Scope is null)
|
||||
return;
|
||||
|
||||
// A native drag delivers no DOM events at all, mouse events included. This fires before a
|
||||
// drag begins, while the pointer still moves freely, which makes it a hint about where the
|
||||
// user is aiming rather than a reliable signal. See the remarks on CatchAllDocuments:
|
||||
this.isComponentHovered = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
|
||||
if (!this.isDefaultZone)
|
||||
this.Logger.LogDebug("The path drop zone '{ZoneId}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.dropZoneId);
|
||||
}
|
||||
|
||||
private void OnMouseLeave(EventArgs _)
|
||||
/// <summary>
|
||||
/// Decides whether the named zone is this one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The area counts as this zone as long as this zone is its default target. That is the whole
|
||||
/// mechanism behind dropping anywhere in a page and still landing here.
|
||||
/// </remarks>
|
||||
/// <param name="zoneId">The ID the hit test reported, or null when it hit nothing.</param>
|
||||
private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the zone, or takes the highlight away.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
|
||||
/// second, and without it every one of them would render every zone on the page anew.
|
||||
/// </remarks>
|
||||
private void ApplyHighlight(bool shouldBeHighlighted)
|
||||
{
|
||||
if(this.Disabled)
|
||||
var highlighted = shouldBeHighlighted && !this.Disabled;
|
||||
if (highlighted == this.isHighlighted)
|
||||
return;
|
||||
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.isHighlighted = highlighted;
|
||||
this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2" : DEFAULT_DRAG_CLASS;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
@if (this.EnableDragDrop)
|
||||
{
|
||||
<div @onmouseenter="@this.OnMouseEnter" @onmouseleave="@this.OnMouseLeave">
|
||||
<div data-drop-zone-id="@this.dropZoneId">
|
||||
<MudPaper Outlined="true" Class="@this.dragClass">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.None" Wrap="Wrap.Wrap">
|
||||
@if (this.ShowAttachedDocumentState && this.hasLoadedFileContent)
|
||||
|
||||
@ -46,17 +46,23 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
public bool EnableDragDrop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Makes this component the default target of its area, meaning of its page, assistant, or
|
||||
/// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
|
||||
/// of their own.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only one zone per area can hold that role, and if several ask for it, the first one in the
|
||||
/// markup gets it. The flag has no effect without drag and drop being enabled.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The area this component lives in, if it lives in one at all.
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
private DropZoneScopeState? Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optionally restricts the file types offered by the native file picker
|
||||
/// and accepted by this component.
|
||||
@ -81,10 +87,12 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
|
||||
private const string DEFAULT_DRAG_CLASS = "relative rounded-lg border-2 border-dashed pa-3 mb-3 mud-width-full";
|
||||
|
||||
private readonly string dropZoneId = $"read-file-content-{Guid.NewGuid():N}";
|
||||
|
||||
private string ButtonText => string.IsNullOrWhiteSpace(this.Text) ? T("Use file content as input") : this.Text;
|
||||
private string dragClass = DEFAULT_DRAG_CLASS;
|
||||
private uint numDropAreasAboveThis;
|
||||
private bool isComponentHovered;
|
||||
private bool isDefaultZone;
|
||||
private bool isHighlighted;
|
||||
private bool isFileDialogOpen;
|
||||
private bool hasLoadedFileContent;
|
||||
private string loadedFileName = string.Empty;
|
||||
@ -120,8 +128,8 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
if (this.EnableDragDrop)
|
||||
{
|
||||
this.ApplyFilters([], [ Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, this.Layer);
|
||||
this.ApplyFilters([], [ Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
|
||||
this.ClaimDefaultZoneRole();
|
||||
}
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
@ -192,10 +200,9 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
||||
|
||||
// Release the drop area. Without this, drop areas below this one would count this component
|
||||
// forever and would stop catching dropped files:
|
||||
if (this.EnableDragDrop)
|
||||
this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, this.Layer).Observe($"{nameof(ReadFileContent)}: releasing the drop area");
|
||||
// Hand the role of the default target back to the area:
|
||||
if (this.isDefaultZone)
|
||||
this.Scope?.ReleaseDefaultZone(this);
|
||||
|
||||
base.DisposeResources();
|
||||
}
|
||||
@ -205,57 +212,73 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
if (!this.EnableDragDrop)
|
||||
return;
|
||||
|
||||
if (this.IsUnavailable && triggeredEvent == Event.TAURI_EVENT_RECEIVED)
|
||||
return;
|
||||
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer)
|
||||
case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
|
||||
this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
|
||||
break;
|
||||
|
||||
case Event.PATHS_DROPPED when data is DroppedPaths dropped:
|
||||
// Whoever the drop was meant for, the drag is over and no zone stays highlighted:
|
||||
this.ApplyHighlight(false);
|
||||
|
||||
if (!this.IsThisZone(dropped.ZoneId))
|
||||
return;
|
||||
|
||||
if (this.IsUnavailable)
|
||||
{
|
||||
this.numDropAreasAboveThis++;
|
||||
this.ClearDragClass();
|
||||
this.Logger.LogDebug("The file zone '{ZoneId}' is unavailable and swallowed {Count} dropped path(s).", this.dropZoneId, dropped.Paths.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
{
|
||||
if(data is int layer && layer > this.Layer && this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if(!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var paths }:
|
||||
if(!this.CanCatchDroppedFile())
|
||||
return;
|
||||
|
||||
await this.LoadFirstValidFile(paths);
|
||||
this.ClearDragClass();
|
||||
await this.LoadFirstValidFile(dropped.Paths);
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Asks the area for the role of its default target, if this component wants it.
|
||||
/// </summary>
|
||||
private void ClaimDefaultZoneRole()
|
||||
{
|
||||
if (!this.CatchAllDocuments || this.Scope is null)
|
||||
return;
|
||||
|
||||
this.isDefaultZone = this.Scope.TryBecomeDefaultZone(this);
|
||||
if (!this.isDefaultZone)
|
||||
this.Logger.LogDebug("The file zone '{ZoneId}' asked to be the default target of its area, which another zone already is. It now takes only the drops aimed at itself.", this.dropZoneId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether the named zone is this one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The area counts as this zone as long as this zone is its default target. That is the whole
|
||||
/// mechanism behind dropping anywhere in an assistant and still landing here.
|
||||
/// </remarks>
|
||||
/// <param name="zoneId">The ID the hit test reported, or null when it hit nothing.</param>
|
||||
private bool IsThisZone(string? zoneId) => zoneId is not null && (zoneId == this.dropZoneId || (this.isDefaultZone && zoneId == this.Scope?.ScopeId));
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the zone, or takes the highlight away.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
|
||||
/// second, and without it every one of them would render every zone on the page anew.
|
||||
/// </remarks>
|
||||
private void ApplyHighlight(bool shouldBeHighlighted)
|
||||
{
|
||||
var highlighted = shouldBeHighlighted && !this.IsUnavailable;
|
||||
if (highlighted == this.isHighlighted)
|
||||
return;
|
||||
|
||||
this.isHighlighted = highlighted;
|
||||
this.dragClass = highlighted ? $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2" : DEFAULT_DRAG_CLASS;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task SelectFile()
|
||||
{
|
||||
@ -417,31 +440,4 @@ public partial class ReadFileContent : MSGComponentBase
|
||||
return string.Format(this.T("Attached file '{0}'."), this.loadedFileName);
|
||||
}
|
||||
|
||||
private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && (this.isComponentHovered || this.CatchAllDocuments);
|
||||
|
||||
private void SetDragClass() => this.dragClass = $"{DEFAULT_DRAG_CLASS} mud-border-primary border-2";
|
||||
|
||||
private void ClearDragClass() => this.dragClass = DEFAULT_DRAG_CLASS;
|
||||
|
||||
private void OnMouseEnter(EventArgs _)
|
||||
{
|
||||
if(this.IsUnavailable || this.numDropAreasAboveThis > 0)
|
||||
return;
|
||||
|
||||
this.Logger.LogDebug("Read file content component is hovered.");
|
||||
this.isComponentHovered = true;
|
||||
this.SetDragClass();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private void OnMouseLeave(EventArgs _)
|
||||
{
|
||||
if(this.IsUnavailable)
|
||||
return;
|
||||
|
||||
this.Logger.LogDebug("Read file content component is no longer hovered.");
|
||||
this.isComponentHovered = false;
|
||||
this.ClearDragClass();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
@if (this.EnableDragDrop)
|
||||
{
|
||||
<PathDropZone Layer="@this.Layer" CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
|
||||
<PathDropZone CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
|
||||
@this.Picker
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the folder here.")
|
||||
|
||||
@ -34,13 +34,9 @@ public partial class SelectDirectory : MSGComponentBase
|
||||
public bool EnableDragDrop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; } = DropLayers.ROOT;
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Makes this component the default target of its area, meaning of its page, assistant, or
|
||||
/// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
|
||||
/// of their own.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
@if (this.EnableDragDrop)
|
||||
{
|
||||
<PathDropZone Layer="@this.Layer" CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
|
||||
<PathDropZone CatchAllDocuments="@this.CatchAllDocuments" Disabled="@this.Disabled" OnPathsDropped="@this.PathsDropped">
|
||||
@this.Picker
|
||||
<MudText Typo="Typo.body2">
|
||||
@T("You can also drag & drop the file here.")
|
||||
|
||||
@ -38,13 +38,9 @@ public partial class SelectFile : MSGComponentBase
|
||||
public bool EnableDragDrop { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// On which layer to register the drop area. Higher layers have priority over lower layers.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int Layer { get; set; } = DropLayers.ROOT;
|
||||
|
||||
/// <summary>
|
||||
/// Catch all documents that are hovered over the AI Studio window and not only over the drop zone.
|
||||
/// Makes this component the default target of its area, meaning of its page, assistant, or
|
||||
/// dialog: it then also takes the drops which land anywhere in that area without hitting a zone
|
||||
/// of their own.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool CatchAllDocuments { get; set; }
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
<DropZoneScope>
|
||||
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
|
||||
@T("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.")
|
||||
</MudJustifiedText>
|
||||
@ -90,7 +92,6 @@
|
||||
</MudJustifiedText>
|
||||
<AttachDocuments
|
||||
Name="ChatTemplateFileAttachments"
|
||||
Layer="@DropLayers.DIALOGS"
|
||||
@bind-DocumentPaths="@this.fileAttachments"
|
||||
UseSmallForm="false"
|
||||
CatchAllDocuments="true"
|
||||
@ -202,6 +203,7 @@
|
||||
}
|
||||
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DropZoneScope>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@if (this.IsReadOnly)
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
<DropZoneScope>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
@ -51,7 +53,7 @@
|
||||
}
|
||||
@if (this.CanChangeSourceAndEmbedding)
|
||||
{
|
||||
<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" Layer="@DropLayers.DIALOGS" 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
|
||||
{
|
||||
@ -191,6 +193,7 @@
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DropZoneScope>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
<DropZoneScope>
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudTextField
|
||||
@ -51,7 +53,7 @@
|
||||
}
|
||||
@if (this.CanChangeSourceAndEmbedding)
|
||||
{
|
||||
<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" Layer="@DropLayers.DIALOGS" 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
|
||||
{
|
||||
@ -191,6 +193,7 @@
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
<Issues IssuesData="@this.dataIssues"/>
|
||||
</DropZoneScope>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
|
||||
@ -2,13 +2,15 @@
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
<DropZoneScope>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@T("See how we load your file. Review the content before we process it further.")
|
||||
</MudJustifiedText>
|
||||
|
||||
@if (this.Document is null)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load file")" FileContent="@this.FileContent" FileContentChanged="@this.ApplyLoadedFileContent" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true"/>
|
||||
<ReadFileContent Text="@T("Load file")" FileContent="@this.FileContent" FileContentChanged="@this.ApplyLoadedFileContent" EnableDragDrop="true" CatchAllDocuments="true"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -100,6 +102,7 @@
|
||||
}
|
||||
</MudTabs>
|
||||
}
|
||||
</DropZoneScope>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
|
||||
|
||||
@ -12,6 +12,8 @@
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
|
||||
<DropZoneScope>
|
||||
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
|
||||
<ConfigurationOption OptionDescription="@T("Preselect batch processing options?")" LabelOn="@T("Batch processing options are preselected")" LabelOff="@T("No batch processing options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectOptions = value)" OptionHelp="@T("When enabled, new batch runs start with the defaults configured below.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
|
||||
|
||||
@ -24,12 +26,12 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Default source of the instructions")" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource)" Data="@this.PromptSourceData" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptSource, out var meta) && meta.IsLocked"/>
|
||||
@if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FREE_PROMPT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load default prompt from file")" FileContent="@this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt" FileContentChanged="@this.UpdateFreePromptFromFileAsync" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" Disabled="@this.FreePromptImportDisabled()"/>
|
||||
<ReadFileContent Text="@T("Load default prompt from file")" FileContent="@this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt" FileContentChanged="@this.UpdateFreePromptFromFileAsync" EnableDragDrop="true" CatchAllDocuments="true" Disabled="@this.FreePromptImportDisabled()"/>
|
||||
<ConfigurationText OptionDescription="@T("Default prompt")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.EditNote" NumLines="5" MaxLines="26" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.FreePrompt = value)" OptionHelp="@T("These instructions are applied to every document of a new batch run.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.FreePrompt, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else if (this.SettingsManager.ConfigurationData.BatchProcessing.PromptSource is BatchProcessingPromptSource.FILE_IMPORT)
|
||||
{
|
||||
<ReadFileContent Text="@T("Load default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" FilePathLoaded="@this.UpdatePromptFilePathAsync" EnableDragDrop="true" Layer="@DropLayers.DIALOGS" CatchAllDocuments="true" Disabled="@this.PromptFileImportDisabled()"/>
|
||||
<ReadFileContent Text="@T("Load default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" FilePathLoaded="@this.UpdatePromptFilePathAsync" EnableDragDrop="true" CatchAllDocuments="true" Disabled="@this.PromptFileImportDisabled()"/>
|
||||
<ConfigurationFile OptionDescription="@T("Default Markdown instructions file")" Disabled="@this.DefaultsDisabled" Icon="@Icons.Material.Filled.Description" Text="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath)" TextUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PromptFilePath = value)" FileDialogTitle="@T("Select the default Markdown instructions file")" Filter="@([FileTypes.MARKDOWN])" OptionHelp="@T("The current content of this Markdown file is loaded whenever the defaults are applied.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PromptFilePath, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
else
|
||||
@ -86,6 +88,7 @@
|
||||
<ConfigurationMinConfidenceSelection Disabled="@this.DefaultsDisabled" RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.MinimumProviderConfidence, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationProviderSelection Component="Components.BATCH_PROCESSING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@this.DefaultsDisabled" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.BatchProcessing.PreselectedProvider = value)" IsLocked="() => ManagedConfiguration.TryGet(x => x.BatchProcessing, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||
</MudPaper>
|
||||
</DropZoneScope>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
@* The chat is a drop area: a file dropped anywhere in it hangs itself on the composer, which is
|
||||
what users are used to. *@
|
||||
<DropZoneScope Class="inner-scrolling-context">
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2" StretchItems="StretchItems.Start">
|
||||
<MudText Typo="Typo.h3">
|
||||
@ -166,4 +168,4 @@
|
||||
</MudDrawerContainer>
|
||||
</MudDrawer>
|
||||
}
|
||||
</div>
|
||||
</DropZoneScope>
|
||||
|
||||
@ -4,7 +4,9 @@
|
||||
@inherits MSGComponentBase
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
@* The page is its own drop target: a plugin archive may be dropped anywhere on it, and there is no
|
||||
inner zone to hand that role to. Hence the page owns the scope state instead of reading it. *@
|
||||
<DropZoneScope State="@this.dropZoneScope" Class="inner-scrolling-context">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h3">
|
||||
@T("Plugins")
|
||||
@ -153,4 +155,4 @@
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
</DropZoneScope>
|
||||
|
||||
@ -43,10 +43,10 @@ public partial class Plugins : MSGComponentBase
|
||||
private bool isSharingPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Number of active drop areas above this page. While there is any, another component owns the
|
||||
/// dropped files and this page must not catch them.
|
||||
/// The drop area of this page. The page owns it rather than reading it from a cascading value,
|
||||
/// because a component cannot read what it cascades itself.
|
||||
/// </summary>
|
||||
private uint numDropAreasAboveThis;
|
||||
private readonly DropZoneScopeState dropZoneScope = new($"plugins-page-{Guid.NewGuid():N}");
|
||||
|
||||
private bool isDraggingOverPage;
|
||||
|
||||
@ -61,10 +61,10 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED, Event.TAURI_EVENT_RECEIVED, Event.REGISTER_FILE_DROP_AREA, Event.UNREGISTER_FILE_DROP_AREA ]);
|
||||
this.ApplyFilters([], [ Event.PLUGINS_RELOADED, Event.CONFIGURATION_CHANGED, Event.HIGHLIGHT_DROP_ZONE, Event.PATHS_DROPPED ]);
|
||||
|
||||
// Register the whole page as a drop area, so users can drop a plugin archive anywhere on it:
|
||||
await this.MessageBus.SendMessage(this, Event.REGISTER_FILE_DROP_AREA, DropLayers.PAGES);
|
||||
// The whole page is the drop target, so users can drop a plugin archive anywhere on it:
|
||||
this.dropZoneScope.TryBecomeDefaultZone(this);
|
||||
|
||||
this.groupConfig = new TableGroupDefinition<IPluginMetadata>
|
||||
{
|
||||
@ -92,8 +92,7 @@ public partial class Plugins : MSGComponentBase
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
// Release the drop area again, so lower layers can catch dropped files:
|
||||
this.MessageBus.SendMessage(this, Event.UNREGISTER_FILE_DROP_AREA, DropLayers.PAGES).Observe($"{nameof(Plugins)}: releasing the drop area");
|
||||
this.dropZoneScope.ReleaseDefaultZone(this);
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
@ -572,39 +571,24 @@ public partial class Plugins : MSGComponentBase
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.REGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int registeredLayer && registeredLayer > DropLayers.PAGES)
|
||||
this.numDropAreasAboveThis++;
|
||||
|
||||
case Event.HIGHLIGHT_DROP_ZONE when data is DropZoneHighlight highlight:
|
||||
this.ApplyHighlight(this.IsThisZone(highlight.ZoneId));
|
||||
break;
|
||||
|
||||
case Event.UNREGISTER_FILE_DROP_AREA when sendingComponent != this:
|
||||
if (data is int unregisteredLayer && unregisteredLayer > DropLayers.PAGES && this.numDropAreasAboveThis > 0)
|
||||
this.numDropAreasAboveThis--;
|
||||
case Event.PATHS_DROPPED when data is DroppedPaths dropped:
|
||||
// Whoever the drop was meant for, the drag is over and nothing stays highlighted:
|
||||
this.ApplyHighlight(false);
|
||||
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_HOVERED }:
|
||||
if (!this.CanCatchDroppedFile())
|
||||
if (!this.IsThisZone(dropped.ZoneId))
|
||||
return;
|
||||
|
||||
this.isDraggingOverPage = true;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_CANCELED }:
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.WINDOW_NOT_FOCUSED }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.FILE_DROP_DROPPED, Payload: var droppedPaths }:
|
||||
this.isDraggingOverPage = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
if (!this.CanCatchDroppedFile())
|
||||
{
|
||||
LOG.LogDebug("The plugins page cannot import right now and swallowed {Count} dropped path(s).", dropped.Paths.Count);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ImportDroppedPluginArchiveAsync(droppedPaths);
|
||||
await this.ImportDroppedPluginArchiveAsync(dropped.Paths);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -612,10 +596,33 @@ public partial class Plugins : MSGComponentBase
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether this page may process dropped files: only when no drop area above it is
|
||||
/// active and when the organization allows importing plugins at all.
|
||||
/// Decides whether the named zone is this page.
|
||||
/// </summary>
|
||||
private bool CanCatchDroppedFile() => this.numDropAreasAboveThis is 0 && this.AllowPluginImport && !this.isImportingAssistantPlugin;
|
||||
/// <param name="zoneId">The ID the hit test reported, or null when it hit nothing.</param>
|
||||
private bool IsThisZone(string? zoneId) => zoneId is not null && zoneId == this.dropZoneScope.ScopeId;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the page as the drop target, or takes that mark away.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The comparison is not for tidiness: a throttled drag-over event arrives about ten times per
|
||||
/// second, and without it every one of them would render the whole plugin table anew.
|
||||
/// </remarks>
|
||||
private void ApplyHighlight(bool shouldBeHighlighted)
|
||||
{
|
||||
var highlighted = shouldBeHighlighted && this.CanCatchDroppedFile();
|
||||
if (highlighted == this.isDraggingOverPage)
|
||||
return;
|
||||
|
||||
this.isDraggingOverPage = highlighted;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides whether this page may process dropped files: only when the organization allows
|
||||
/// importing plugins at all and no import is running.
|
||||
/// </summary>
|
||||
private bool CanCatchDroppedFile() => this.AllowPluginImport && !this.isImportingAssistantPlugin;
|
||||
|
||||
/// <summary>
|
||||
/// Imports a plugin archive the user dropped onto the page. Anything that is not exactly one
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static class DropLayers
|
||||
{
|
||||
public const int ROOT = 0;
|
||||
|
||||
public const int PAGES = 10;
|
||||
public const int ASSISTANTS = 20;
|
||||
|
||||
public const int DIALOGS = 100;
|
||||
}
|
||||
57
app/MindWork AI Studio/Tools/DropZoneScopeState.cs
Normal file
57
app/MindWork AI Studio/Tools/DropZoneScopeState.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// The shared state of one drop zone scope, meaning one page, one assistant, or one dialog.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A scope is the area whose drops end up at its default target whenever the cursor is not over a
|
||||
/// more specific zone. That is what users are used to: a file dropped anywhere in the chat hangs
|
||||
/// itself on the composer. This object connects the two sides -- the scope cascades it inwards, and
|
||||
/// the zone which wants to be the default target claims it here.
|
||||
/// </remarks>
|
||||
/// <param name="scopeId">The ID of the element the scope renders.</param>
|
||||
public sealed class DropZoneScopeState(string scopeId)
|
||||
{
|
||||
/// <summary>
|
||||
/// The ID of the element the scope renders. The hit test reports it for every point inside the
|
||||
/// area which no more specific zone covers.
|
||||
/// </summary>
|
||||
public string ScopeId { get; } = scopeId;
|
||||
|
||||
private object? defaultZone;
|
||||
|
||||
/// <summary>
|
||||
/// Makes the given zone the default target of this scope, unless another zone was there first.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Zones initialize in render order, so the first one in the markup wins. Two zones asking for
|
||||
/// the same area is a mistake in the markup rather than a state worth resolving, and taking the
|
||||
/// first one is at least a rule which can be stated and logged. Asking twice is no mistake,
|
||||
/// though: a zone whose parameters are set anew has to keep the role it already holds.
|
||||
/// </remarks>
|
||||
/// <param name="zone">The zone that wants to be the default target.</param>
|
||||
/// <returns>True if the zone is the default target of this scope from now on.</returns>
|
||||
public bool TryBecomeDefaultZone(object zone)
|
||||
{
|
||||
if (this.defaultZone is not null && !ReferenceEquals(this.defaultZone, zone))
|
||||
return false;
|
||||
|
||||
this.defaultZone = zone;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gives the role of the default target up again so that another zone can take it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every zone that took the role has to do this when it is disposed. Without it, an area would
|
||||
/// lose its default target for good as soon as the zone holding it is created anew -- which is
|
||||
/// what happens on every navigation and every time a dialog is opened again.
|
||||
/// </remarks>
|
||||
/// <param name="zone">The zone that gives the role up.</param>
|
||||
public void ReleaseDefaultZone(object zone)
|
||||
{
|
||||
if (ReferenceEquals(this.defaultZone, zone))
|
||||
this.defaultZone = null;
|
||||
}
|
||||
}
|
||||
@ -213,16 +213,6 @@ public enum Event
|
||||
// File attachment events:
|
||||
//
|
||||
|
||||
/// <summary>
|
||||
/// Registers a file drop area for file attachment handling.
|
||||
/// </summary>
|
||||
REGISTER_FILE_DROP_AREA,
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a file drop area from file attachment handling.
|
||||
/// </summary>
|
||||
UNREGISTER_FILE_DROP_AREA,
|
||||
|
||||
/// <summary>
|
||||
/// Names the drop zone under the cursor of a running drag so that exactly this one is highlighted.
|
||||
/// </summary>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user