diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b3ba0be3b..ac458bf7a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to ### Added +- Ctrl/Cmd + Enter to Send a Message to the AI Assistant + [#2406](https://github.com/OpenFn/lightning/issues/2406) - Add styles to AI chat messages [#2484](https://github.com/OpenFn/lightning/issues/2484) - Auditing when enabling/disabling a workflow diff --git a/assets/js/hooks/index.ts b/assets/js/hooks/index.ts index acc39029ec..69e08544fa 100644 --- a/assets/js/hooks/index.ts +++ b/assets/js/hooks/index.ts @@ -470,22 +470,52 @@ export const BlurDataclipEditor = { } as PhoenixHook; /** - * Factory function to create a hook for listening to specific key combinations. + * Factory function to create a hook for listening to specific key combinations, + * only triggering when the element or its children are focused. * * @param keyCheck - Function to check if a keyboard event matches the desired key combination. * @param action - Action function to be executed when the keyCheck condition is satisfied. + * @param isDefault - Whether this hook should fire when no other elements are focused. * @returns - A PhoenixHook with mounted and destroyed lifecycles. */ function createKeyCombinationHook( keyCheck: (e: KeyboardEvent) => boolean, - action: (e: KeyboardEvent, el: HTMLElement) => void + action: (e: KeyboardEvent, el: HTMLElement) => void, + isDefault: boolean = false ): PhoenixHook { return { mounted() { this.callback = (e: KeyboardEvent) => { - if (keyCheck(e)) { + if (!keyCheck(e)) return; + + const targetEl = e.target as HTMLElement; + const isFocusedWithin = + this.el.contains(targetEl) || + (this.el.getAttribute('form') && + document + .getElementById(this.el.getAttribute('form')) + ?.contains(targetEl)); + + if (isFocusedWithin) { e.preventDefault(); action(e, this.el); + return; + } + + if (isDefault) { + const focusedElement = document.activeElement; + const isHandledByOtherHook = + document.querySelector('[phx-hook]')?.contains(focusedElement) || + Array.from(document.querySelectorAll('[phx-hook][form]')).some(el => + document + .getElementById(el.getAttribute('form')) + ?.contains(focusedElement) + ); + + if (!isHandledByOtherHook) { + e.preventDefault(); + action(e, this.el); + } } }; window.addEventListener('keydown', this.callback); @@ -548,16 +578,29 @@ export const SaveViaCtrlS = createKeyCombinationHook( ); /** - * Hook to trigger a save and run action on the job panel when the Ctrl (or Cmd on Mac) + Enter key combination is pressed. + * Hook to send a message to the AI Chat when the Ctrl (or Cmd on Mac) + Enter key combination is pressed + * while the chat form or its elements are focused. + */ +export const SendMessageViaCtrlEnter = createKeyCombinationHook( + isCtrlOrMetaEnter, + clickAction, + false +); + +/** + * Hook to trigger a save and run action on the job panel when the Ctrl (or Cmd on Mac) + Enter key combination is pressed + * while the job panel or its elements are focused, or when no other specific elements are focused. */ export const DefaultRunViaCtrlEnter = createKeyCombinationHook( isCtrlOrMetaEnter, - clickAction + clickAction, + true ); export const AltRunViaCtrlShiftEnter = createKeyCombinationHook( isCtrlOrMetaShiftEnter, - clickAction + clickAction, + true ); /** diff --git a/lib/lightning_web/live/workflow_live/ai_assistant_component.ex b/lib/lightning_web/live/workflow_live/ai_assistant_component.ex index 6d609f78c5..843dbcc72e 100644 --- a/lib/lightning_web/live/workflow_live/ai_assistant_component.ex +++ b/lib/lightning_web/live/workflow_live/ai_assistant_component.ex @@ -451,6 +451,8 @@ defmodule LightningWeb.WorkflowLive.AiAssistantComponent do type="submit" disabled={@disabled} tooltip={@tooltip} + phx-hook="SendMessageViaCtrlEnter" + form="ai-assistant-form" > Send