Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions app/MindWork AI Studio/Assistants/I18N/allTexts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1675,6 +1675,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T20906218
-- Use app default
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONPROVIDERSELECTION::T3672477670"] = "Use app default"

-- No shortcut configured
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T3099115336"] = "No shortcut configured"

-- Change shortcut
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T4081853237"] = "Change shortcut"

-- Configure Keyboard Shortcut
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONSHORTCUT::T636303786"] = "Configure Keyboard Shortcut"

-- Yes, let the AI decide which data sources are needed.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T1031370894"] = "Yes, let the AI decide which data sources are needed."

Expand Down Expand Up @@ -2017,6 +2026,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
-- Do you want to show preview features in the app?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"

-- Voice recording shortcut
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1278320412"] = "Voice recording shortcut"

-- How often should we check for app updates?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1364944735"] = "How often should we check for app updates?"

Expand Down Expand Up @@ -2047,6 +2059,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1898060643"]
-- Select the language for the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1907446663"] = "Select the language for the app."

-- The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2143741496"] = "The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused."

-- Disable dictation and transcription
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T215381891"] = "Disable dictation and transcription"

Expand Down Expand Up @@ -4612,6 +4627,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T3832
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T4004501229"] = "Preselect one of your profiles?"

-- Save
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1294818664"] = "Save"

-- Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1464973299"] = "Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused."

-- Press a key combination...
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1468443151"] = "Press a key combination..."

-- Clear Shortcut
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T1807313248"] = "Clear Shortcut"

-- Invalid shortcut: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T189893682"] = "Invalid shortcut: {0}"

-- This shortcut conflicts with: {0}
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T2633102934"] = "This shortcut conflicts with: {0}"

-- Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd).
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3060573513"] = "Please include at least one modifier key (Ctrl, Shift, Alt, or Cmd)."

-- Shortcut is valid and available.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3159532525"] = "Shortcut is valid and available."

-- Define a shortcut
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T3734850493"] = "Define a shortcut"

-- Supported modifiers: Ctrl/Cmd, Shift, Alt.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T889258890"] = "Supported modifiers: Ctrl/Cmd, Shift, Alt."

-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SHORTCUTDIALOG::T900713019"] = "Cancel"

-- Please enter a value.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SINGLEINPUTDIALOG::T3576780391"] = "Please enter a value."

Expand Down
2 changes: 1 addition & 1 deletion app/MindWork AI Studio/Components/ChatComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
OnAdornmentClick="() => this.SendMessage()"
Disabled="@this.IsInputForbidden()"
Immediate="@true"
OnKeyUp="this.InputKeyEvent"
OnKeyUp="@this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Class="@this.UserInputClass"
Style="@this.UserInputStyle"/>
Expand Down
26 changes: 26 additions & 0 deletions app/MindWork AI Studio/Components/ConfigurationShortcut.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@inherits ConfigurationBaseCore

<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@this.Icon" Color="@this.IconColor"/>
<MudText Typo="Typo.body1" Class="flex-grow-1">
@if (string.IsNullOrWhiteSpace(this.Shortcut()))
{
@T("No shortcut configured")
}
else
{
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Outlined">
@this.GetDisplayShortcut()
</MudChip>
}
</MudText>
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Edit"
OnClick="@this.OpenDialog"
Disabled="@this.IsDisabled"
Class="mb-1">
@T("Change shortcut")
</MudButton>
</MudStack>
109 changes: 109 additions & 0 deletions app/MindWork AI Studio/Components/ConfigurationShortcut.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using AIStudio.Dialogs;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;

using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;

namespace AIStudio.Components;

/// <summary>
/// A configuration component for capturing and displaying keyboard shortcuts.
/// </summary>
public partial class ConfigurationShortcut : ConfigurationBaseCore
{
[Inject]
private IDialogService DialogService { get; init; } = null!;

[Inject]
private RustService RustService { get; init; } = null!;

/// <summary>
/// The current shortcut value.
/// </summary>
[Parameter]
public Func<string> Shortcut { get; set; } = () => string.Empty;

/// <summary>
/// An action which is called when the shortcut was changed.
/// </summary>
[Parameter]
public Action<string> ShortcutUpdate { get; set; } = _ => { };

/// <summary>
/// The name/identifier of the shortcut (used for conflict detection and registration).
/// </summary>
[Parameter]
public Shortcut ShortcutId { get; init; }

/// <summary>
/// The icon to display.
/// </summary>
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.Keyboard;

/// <summary>
/// The color of the icon.
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Default;

#region Overrides of ConfigurationBase

protected override bool Stretch => true;

protected override Variant Variant => Variant.Outlined;

protected override string Label => this.OptionDescription;

#endregion

private string GetDisplayShortcut()
{
var shortcut = this.Shortcut();
if (string.IsNullOrWhiteSpace(shortcut))
return string.Empty;

// Convert internal format to display format:
return shortcut
.Replace("CmdOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl")
.Replace("CommandOrControl", OperatingSystem.IsMacOS() ? "Cmd" : "Ctrl");
}

private async Task OpenDialog()
{
// Suspend shortcut processing while the dialog is open, so the user can
// press the current shortcut to re-enter it without triggering the action:
await this.RustService.SuspendShortcutProcessing();

try
{
var dialogParameters = new DialogParameters<ShortcutDialog>
{
{ x => x.InitialShortcut, this.Shortcut() },
{ x => x.ShortcutId, this.ShortcutId },
};

var dialogReference = await this.DialogService.ShowAsync<ShortcutDialog>(
this.T("Configure Keyboard Shortcut"),
dialogParameters,
DialogOptions.FULLSCREEN);

var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;

if (dialogResult.Data is string newShortcut)
{
this.ShortcutUpdate(newShortcut);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
}
finally
{
// Resume the shortcut processing when the dialog is closed:
await this.RustService.ResumeShortcutProcessing();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@using AIStudio.Tools.Rust
@inherits SettingsPanelBase

<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="@T("App Options")">
Expand Down Expand Up @@ -33,5 +34,6 @@
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
<ConfigurationShortcut ShortcutId="Shortcut.VOICE_RECORDING_TOGGLE" OptionDescription="@T("Voice recording shortcut")" Shortcut="@(() => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording)" ShortcutUpdate="@(shortcut => this.SettingsManager.ConfigurationData.App.ShortcutVoiceRecording = shortcut)" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
}
</ExpansionPanel>
40 changes: 40 additions & 0 deletions app/MindWork AI Studio/Components/VoiceRecorder.razor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using AIStudio.Provider;
using AIStudio.Tools.MIME;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;

using Microsoft.AspNetCore.Components;
Expand All @@ -24,6 +25,9 @@ public partial class VoiceRecorder : MSGComponentBase

protected override async Task OnInitializedAsync()
{
// Register for global shortcut events:
this.ApplyFilters([], [Event.TAURI_EVENT_RECEIVED]);

await base.OnInitializedAsync();

try
Expand All @@ -37,6 +41,38 @@ protected override async Task OnInitializedAsync()
}
}

protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
switch (triggeredEvent)
{
case Event.TAURI_EVENT_RECEIVED when data is TauriEvent { EventType: TauriEventType.GLOBAL_SHORTCUT_PRESSED } tauriEvent:
// Check if this is the voice recording toggle shortcut:
if (tauriEvent.TryGetShortcut(out var shortcutId) && shortcutId == Shortcut.VOICE_RECORDING_TOGGLE)
{
this.Logger.LogInformation("Global shortcut triggered for voice recording toggle.");
await this.ToggleRecordingFromShortcut();
}

break;
}
}

/// <summary>
/// Toggles the recording state when triggered by a global shortcut.
/// </summary>
private async Task ToggleRecordingFromShortcut()
{
// Don't allow toggle if transcription is in progress or preparing:
if (this.isTranscribing || this.isPreparing)
{
this.Logger.LogDebug("Ignoring shortcut: transcription or preparation is in progress.");
return;
}

// Toggle the recording state:
await this.OnRecordingToggled(!this.isRecording);
}

#endregion

private uint numReceivedChunks;
Expand Down Expand Up @@ -109,6 +145,10 @@ private async Task OnRecordingToggled(bool toggled)
// Clean up the recording stream if starting failed:
await this.FinalizeRecordingStream();
}
finally
{
this.StateHasChanged();
}
}
else
{
Expand Down
50 changes: 50 additions & 0 deletions app/MindWork AI Studio/Dialogs/ShortcutDialog.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@inherits MSGComponentBase

<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Press the desired key combination to set the shortcut. The shortcut will be registered globally and will work even when the app is not focused.")
</MudJustifiedText>

<MudFocusTrap DefaultFocus="DefaultFocus.FirstChild">
<MudTextField
@ref="@this.inputField"
T="string"
Text="@this.ShowText"
Variant="Variant.Outlined"
Label="@T("Define a shortcut")"
Placeholder="@T("Press a key combination...")"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Keyboard"
Immediate="@true"
TextUpdateSuppression="false"
OnKeyDown="@this.HandleKeyDown"
OnBlur="@this.HandleBlur"
UserAttributes="@USER_INPUT_ATTRIBUTES"
AutoFocus="true"
KeyDownPreventDefault="true"
KeyUpPreventDefault="true"
HelperText="@T("Supported modifiers: Ctrl/Cmd, Shift, Alt.")"
Class="me-3"/>
</MudFocusTrap>

@if (!string.IsNullOrWhiteSpace(this.validationMessage))
{
<MudAlert Severity="@this.validationSeverity" Variant="Variant.Filled" Class="mb-3">
@this.validationMessage
</MudAlert>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.ClearShortcut" Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Clear">
@T("Clear Shortcut")
</MudButton>
<MudSpacer/>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Confirm" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.hasValidationError">
@T("Save")
</MudButton>
</DialogActions>
</MudDialog>
Loading