From bf3b267bc946e96e4ced3b221d6f2090c72d0a06 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 13 Apr 2023 17:23:20 -0600 Subject: [PATCH 1/4] feat(cmdk): shell-style message history for up/down keys --- .../ui/src/components/Command/AiCommand.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index 5e816dd7d53..fa0f8077811 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -343,6 +343,23 @@ const AiCommand = () => { setIsLoading, }) + const inputRef = useRef(null) + + // Message index (of role === 'user') when hitting up/down on the keyboard (shell style) + const [, setMessageSelectionIndex] = useState(0) + + const userMessages = messages.filter(({ role }) => role === MessageRole.User) + + useEffect(() => { + if (isResponding) { + return + } + + // Note: intentionally setting index to 1 greater than array length + setMessageSelectionIndex(userMessages.length) + console.log('setting index to', userMessages.length) + }, [messages, isResponding]) + const handleSubmit = useCallback( (message: string) => { setSearch('') @@ -362,6 +379,19 @@ const AiCommand = () => { } }, []) + // Focus the input when typing from anywhere + useEffect(() => { + function onKeyDown() { + inputRef.current?.focus() + } + + window.addEventListener('keydown', onKeyDown) + + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, []) + return (
e.stopPropagation()}>
@@ -456,6 +486,7 @@ const AiCommand = () => {
{ } handleSubmit(search) return + case 'ArrowUp': + setMessageSelectionIndex((index) => { + const newIndex = Math.max(index - 1, 0) + setSearch(userMessages[newIndex]?.content ?? '') + return newIndex + }) + return + case 'ArrowDown': + setMessageSelectionIndex((index) => { + const newIndex = Math.min(index + 1, userMessages.length) + setSearch(userMessages[newIndex]?.content ?? '') + return newIndex + }) + return default: return } From e93fe05154cdaa6afab07b9f0562588627de8de1 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 13 Apr 2023 17:47:02 -0600 Subject: [PATCH 2/4] refactor(cmdk): shell-style message history into useHistoryKeys hook --- .../ui/src/components/Command/AiCommand.tsx | 39 ++++---------- .../src/components/Command/Command.utils.tsx | 53 +++++++++++++++++++ .../Command/GenerateSQL/GenerateSQL.tsx | 10 +++- 3 files changed, 71 insertions(+), 31 deletions(-) diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index fa0f8077811..e713c95a525 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -17,7 +17,7 @@ import { SSE } from 'sse.js' import { Button, IconAlertTriangle, IconCornerDownLeft, IconUser, Input } from 'ui' import { AiIcon, AiIconChat } from './Command.icons' -import { CommandGroup, CommandItem } from './Command.utils' +import { CommandGroup, CommandItem, useHistoryKeys } from './Command.utils' import { useCommandMenu } from './CommandMenuProvider' @@ -343,23 +343,16 @@ const AiCommand = () => { setIsLoading, }) + useHistoryKeys({ + enable: !isResponding, + messages: messages + .filter(({ role }) => role === MessageRole.User) + .map(({ content }) => content), + setPrompt: setSearch, + }) + const inputRef = useRef(null) - // Message index (of role === 'user') when hitting up/down on the keyboard (shell style) - const [, setMessageSelectionIndex] = useState(0) - - const userMessages = messages.filter(({ role }) => role === MessageRole.User) - - useEffect(() => { - if (isResponding) { - return - } - - // Note: intentionally setting index to 1 greater than array length - setMessageSelectionIndex(userMessages.length) - console.log('setting index to', userMessages.length) - }, [messages, isResponding]) - const handleSubmit = useCallback( (message: string) => { setSearch('') @@ -524,20 +517,6 @@ const AiCommand = () => { } handleSubmit(search) return - case 'ArrowUp': - setMessageSelectionIndex((index) => { - const newIndex = Math.max(index - 1, 0) - setSearch(userMessages[newIndex]?.content ?? '') - return newIndex - }) - return - case 'ArrowDown': - setMessageSelectionIndex((index) => { - const newIndex = Math.min(index + 1, userMessages.length) - setSearch(userMessages[newIndex]?.content ?? '') - return newIndex - }) - return default: return } diff --git a/packages/ui/src/components/Command/Command.utils.tsx b/packages/ui/src/components/Command/Command.utils.tsx index 6c3fc617479..b42dd2cd370 100644 --- a/packages/ui/src/components/Command/Command.utils.tsx +++ b/packages/ui/src/components/Command/Command.utils.tsx @@ -324,3 +324,56 @@ export const TextHighlighter = ({ text, query, ...props }: TextHighlighterProps) } TextHighlighter.displayName = 'TextHighlighter' + +export interface UseHistoryKeysOptions { + enable: boolean + messages: string[] + setPrompt: (prompt: string) => void +} + +/** + * Enables a shell-style message history when hitting + * up/down on the keyboard + */ +export function useHistoryKeys({ enable, messages, setPrompt }: UseHistoryKeysOptions) { + // Message index when hitting up/down on the keyboard (shell style) + const [, setMessageSelectionIndex] = React.useState(0) + + React.useEffect(() => { + if (enable) { + return + } + + // Note: intentionally setting index to 1 greater than array length + setMessageSelectionIndex(messages.length) + }, [messages, enable]) + + React.useEffect(() => { + function onKeyDown(e: KeyboardEvent) { + switch (e.key) { + case 'ArrowUp': + setMessageSelectionIndex((index) => { + const newIndex = Math.max(index - 1, 0) + setPrompt(messages[newIndex] ?? '') + return newIndex + }) + return + case 'ArrowDown': + setMessageSelectionIndex((index) => { + const newIndex = Math.min(index + 1, messages.length) + setPrompt(messages[newIndex] ?? '') + return newIndex + }) + return + default: + return + } + } + + window.addEventListener('keydown', onKeyDown) + + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, [messages]) +} diff --git a/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx b/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx index bd018484210..f933c818963 100644 --- a/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx +++ b/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx @@ -15,7 +15,7 @@ import { import { cn } from '../../../utils/cn' import { AiIcon, AiIconChat } from '../Command.icons' -import { CommandItem } from '../Command.utils' +import { CommandItem, useHistoryKeys } from '../Command.utils' import { useCommandMenu } from '../CommandMenuProvider' import { SAMPLE_QUERIES } from '../Command.constants' import SQLOutputActions from './SQLOutputActions' @@ -50,6 +50,14 @@ const GenerateSQL = () => { setIsLoading, }) + useHistoryKeys({ + enable: !isResponding, + messages: messages + .filter(({ role }) => role === MessageRole.User) + .map(({ content }) => content), + setPrompt: setSearch, + }) + const handleSubmit = useCallback( (message: string) => { setSearch('') From 055814070e4fd6c5c63d91be8e5bd23507a775f3 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 13 Apr 2023 18:02:14 -0600 Subject: [PATCH 3/4] refactor(cmdk): hook to auto focus input on key press --- .../ui/src/components/Command/AiCommand.tsx | 19 ++-------- .../src/components/Command/Command.utils.tsx | 38 +++++++++++++++++++ .../Command/GenerateSQL/GenerateSQL.tsx | 14 ++----- 3 files changed, 45 insertions(+), 26 deletions(-) diff --git a/packages/ui/src/components/Command/AiCommand.tsx b/packages/ui/src/components/Command/AiCommand.tsx index e713c95a525..d1b76214643 100644 --- a/packages/ui/src/components/Command/AiCommand.tsx +++ b/packages/ui/src/components/Command/AiCommand.tsx @@ -17,7 +17,7 @@ import { SSE } from 'sse.js' import { Button, IconAlertTriangle, IconCornerDownLeft, IconUser, Input } from 'ui' import { AiIcon, AiIconChat } from './Command.icons' -import { CommandGroup, CommandItem, useHistoryKeys } from './Command.utils' +import { CommandGroup, CommandItem, useAutoInputFocus, useHistoryKeys } from './Command.utils' import { useCommandMenu } from './CommandMenuProvider' @@ -343,6 +343,8 @@ const AiCommand = () => { setIsLoading, }) + const inputRef = useAutoInputFocus() + useHistoryKeys({ enable: !isResponding, messages: messages @@ -351,8 +353,6 @@ const AiCommand = () => { setPrompt: setSearch, }) - const inputRef = useRef(null) - const handleSubmit = useCallback( (message: string) => { setSearch('') @@ -372,19 +372,6 @@ const AiCommand = () => { } }, []) - // Focus the input when typing from anywhere - useEffect(() => { - function onKeyDown() { - inputRef.current?.focus() - } - - window.addEventListener('keydown', onKeyDown) - - return () => { - window.removeEventListener('keydown', onKeyDown) - } - }, []) - return (
e.stopPropagation()}>
diff --git a/packages/ui/src/components/Command/Command.utils.tsx b/packages/ui/src/components/Command/Command.utils.tsx index b42dd2cd370..1b2a423a997 100644 --- a/packages/ui/src/components/Command/Command.utils.tsx +++ b/packages/ui/src/components/Command/Command.utils.tsx @@ -377,3 +377,41 @@ export function useHistoryKeys({ enable, messages, setPrompt }: UseHistoryKeysOp } }, [messages]) } + +/** + * Automatically focuses an input on key press + * and on load (after the call stack) + * + * @returns An input ref for the input to focus + */ +export function useAutoInputFocus() { + const [input, setInput] = React.useState() + + // Use a callback-style ref to access the element when it mounts + const inputRef = React.useCallback((inputElement: HTMLInputElement) => { + if (inputElement) { + setInput(inputElement) + + // We need to delay the focus until the end of the call stack + // due to order of operations + setTimeout(() => { + inputElement.focus() + }, 0) + } + }, []) + + // Focus the input when typing from anywhere + React.useEffect(() => { + function onKeyDown() { + input?.focus() + } + + window.addEventListener('keydown', onKeyDown) + + return () => { + window.removeEventListener('keydown', onKeyDown) + } + }, [input]) + + return inputRef +} diff --git a/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx b/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx index f933c818963..4e540a6ad60 100644 --- a/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx +++ b/packages/ui/src/components/Command/GenerateSQL/GenerateSQL.tsx @@ -15,7 +15,7 @@ import { import { cn } from '../../../utils/cn' import { AiIcon, AiIconChat } from '../Command.icons' -import { CommandItem, useHistoryKeys } from '../Command.utils' +import { CommandItem, useAutoInputFocus, useHistoryKeys } from '../Command.utils' import { useCommandMenu } from '../CommandMenuProvider' import { SAMPLE_QUERIES } from '../Command.constants' import SQLOutputActions from './SQLOutputActions' @@ -50,6 +50,8 @@ const GenerateSQL = () => { setIsLoading, }) + const inputRef = useAutoInputFocus() + useHistoryKeys({ enable: !isResponding, messages: messages @@ -290,15 +292,7 @@ const GenerateSQL = () => { )} { - if (inputElement) { - // We need to delay the focus until the end of the call stack - // due to order of operations - setTimeout(() => { - inputElement.focus() - }, 0) - } - }} + inputRef={inputRef} className="bg-scale-100 rounded mx-3" autoFocus placeholder={ From 6ed13325fbd4a731aa8c4911462dd1b6f5ebc535 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 13 Apr 2023 18:09:00 -0600 Subject: [PATCH 4/4] chore(cmdk): useHistoryKeys comment clarification --- packages/ui/src/components/Command/Command.utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/Command/Command.utils.tsx b/packages/ui/src/components/Command/Command.utils.tsx index 1b2a423a997..e63343a226e 100644 --- a/packages/ui/src/components/Command/Command.utils.tsx +++ b/packages/ui/src/components/Command/Command.utils.tsx @@ -344,7 +344,7 @@ export function useHistoryKeys({ enable, messages, setPrompt }: UseHistoryKeysOp return } - // Note: intentionally setting index to 1 greater than array length + // Note: intentionally setting index to 1 greater than max index setMessageSelectionIndex(messages.length) }, [messages, enable])