diff --git a/packages/ui-patterns/AssistantChat/AssistantChatCommands.stories.tsx b/packages/ui-patterns/AssistantChat/AssistantChatCommands.stories.tsx
new file mode 100644
index 00000000000..115209b17c4
--- /dev/null
+++ b/packages/ui-patterns/AssistantChat/AssistantChatCommands.stories.tsx
@@ -0,0 +1,90 @@
+import { StoryObj } from '@storybook/react'
+import { AssistantChatForm } from './AssistantChatForm'
+import { AssistantCommandsPopover } from './AssistantCommandsPopover'
+import { Box, User } from 'lucide-react'
+import { createRef, useRef, useState } from 'react'
+
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
+export default {
+ title: 'AI/Assistant Chat',
+ component: AssistantChatForm,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
+ layout: 'centered',
+ },
+ decorators: [
+ (Story: any) => {
+ return (
+
+
+
+ )
+ },
+ ],
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
+ tags: ['autodocs'],
+ // More on argTypes: https://storybook.js.org/docs/api/argtypes
+ // argTypes: {},
+}
+
+// export default meta
+
+type Story = StoryObj
+
+export const CommandsPopover: Story = {
+ render: function Render(args) {
+ const [commandsOpen, setCommandsOpen] = useState(false)
+ const textAreaRef = createRef()
+ const [value, setValueState] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ function handleSubmit(event: React.FormEvent) {
+ // set loading for 3 seconds and then reset
+ setLoading(true)
+ setTimeout(() => {
+ setLoading(false)
+ }, 1500)
+ }
+
+ const suggestions = [
+ 'Add policy for org Inserted User Access',
+ 'Add policy for User-Specific Todo Access',
+ 'Add policy for Org Update Restriction',
+ ]
+
+ return (
+ <>
+ setValueState(e)}
+ suggestions={suggestions}
+ >
+ setValueState(e.target.value)}
+ commandsOpen={commandsOpen}
+ setCommandsOpen={setCommandsOpen}
+ onSubmit={async (event) => {
+ event.preventDefault()
+ handleSubmit(event)
+ }}
+ />
+
+
+ Press / to open
+ commands
+
+ >
+ )
+ },
+ args: {
+ // placeholder: 'Ask AI a question...',
+ },
+}
diff --git a/packages/ui-patterns/AssistantChat/AssistantChatForm.stories.tsx b/packages/ui-patterns/AssistantChat/AssistantChatForm.stories.tsx
new file mode 100644
index 00000000000..cb643aeaac2
--- /dev/null
+++ b/packages/ui-patterns/AssistantChat/AssistantChatForm.stories.tsx
@@ -0,0 +1,66 @@
+import { StoryObj } from '@storybook/react'
+import { AssistantChatForm } from './AssistantChatForm'
+import { Box, User } from 'lucide-react'
+import { createRef, useRef, useState } from 'react'
+
+// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
+export default {
+ title: 'AI/Assistant Chat',
+ component: AssistantChatForm,
+ parameters: {
+ // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
+ layout: 'centered',
+ },
+ decorators: [
+ (Story: any) => {
+ return (
+
+
+
+ )
+ },
+ ],
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
+ tags: ['autodocs'],
+ // More on argTypes: https://storybook.js.org/docs/api/argtypes
+ // argTypes: {},
+}
+
+// export default meta
+
+type Story = StoryObj
+
+export const Primary: Story = {
+ render: function Render(args) {
+ const textAreaRef = createRef()
+ const [value, setValueState] = useState('')
+ const [loading, setLoading] = useState(false)
+
+ function handleSubmit(event: React.FormEvent) {
+ // set loading for 3 seconds and then reset
+ setLoading(true)
+ setTimeout(() => {
+ setLoading(false)
+ }, 1500)
+ }
+
+ return (
+ }
+ value={value}
+ loading={loading}
+ disabled={loading}
+ onValueChange={(e) => setValueState(e.target.value)}
+ onSubmit={async (event) => {
+ event.preventDefault()
+ handleSubmit(event)
+ }}
+ />
+ )
+ },
+ args: {
+ // placeholder: 'Ask AI a question...',
+ },
+}
diff --git a/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx b/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx
new file mode 100644
index 00000000000..4f4190aa723
--- /dev/null
+++ b/packages/ui-patterns/AssistantChat/AssistantChatForm.tsx
@@ -0,0 +1,148 @@
+import { Loader2 } from 'lucide-react'
+import React, { ChangeEvent, createRef, useEffect } from 'react'
+import { TextArea } from 'ui/src/components/shadcn/ui/text-area'
+import { cn } from 'ui/src/lib/utils'
+
+export interface FormProps extends React.FormHTMLAttributes {
+ /* The ref for the textarea */
+ textAreaRef: React.RefObject
+ /* The loading state of the form */
+ loading: boolean
+ /* The disabled state of the form */
+ disabled?: boolean
+ /* The value of the textarea */
+ value?: string
+ /* The function to handle the value change */
+ onValueChange: (value: ChangeEvent) => void
+ /* Used to stop onSubmit event when Command popover is open. Use with AssistantCommandsPopover */
+ commandsOpen?: boolean
+ /* Used to close Command popover when onSubmit event happens. Use with AssistantCommandsPopover */
+ setCommandsOpen?: (value: boolean) => void
+ /* The icon to display on the left of the textarea */
+ icon?: React.ReactNode
+ /* The function to handle the form submission */
+ onSubmit: React.FormHTMLAttributes['onSubmit']
+}
+
+const AssistantChatForm = React.forwardRef(
+ (
+ {
+ loading = false,
+ disabled = false,
+ value = '',
+ textAreaRef,
+ commandsOpen = false,
+ icon = null,
+ onValueChange,
+ setCommandsOpen,
+ onSubmit,
+ ...props
+ },
+ ref
+ ) => {
+ const formRef = createRef()
+ const submitRef = createRef()
+
+ /**
+ * This effect is used to resize the textarea based on the content
+ */
+ useEffect(() => {
+ if (textAreaRef) {
+ if (!value && textAreaRef && textAreaRef.current) {
+ textAreaRef.current.style.height = '40px'
+ } else if (textAreaRef && textAreaRef.current) {
+ const newHeight = textAreaRef.current.scrollHeight + 'px'
+ textAreaRef.current.style.height = newHeight
+ }
+ }
+ }, [value, textAreaRef])
+
+ /**
+ * This effect is used to focus the textarea when the component mounts
+ */
+ useEffect(() => {
+ textAreaRef?.current?.focus()
+ }, [value, textAreaRef])
+
+ /**
+ * This function is used to handle the "Enter" key press
+ */
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ // Check if the pressed key is "Enter" (key code 13) without the "Shift" key
+ // also checks if the commands popover is open
+ if (event.key === 'Enter' && !event.shiftKey && !commandsOpen) {
+ event.preventDefault()
+ if (submitRef.current) {
+ submitRef.current.click()
+ }
+ }
+
+ // handles closing the commands popover if open
+ if (event.key === 'Enter' && commandsOpen) {
+ if (setCommandsOpen) setCommandsOpen(false)
+ }
+ }
+
+ return (
+
+ )
+ }
+)
+
+AssistantChatForm.displayName = 'AssistantChatForm'
+
+export { AssistantChatForm }
diff --git a/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx b/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx
new file mode 100644
index 00000000000..5b07624edac
--- /dev/null
+++ b/packages/ui-patterns/AssistantChat/AssistantCommandsPopover.tsx
@@ -0,0 +1,221 @@
+'use client'
+
+import {
+ ComponentPropsWithoutRef,
+ ElementRef,
+ createRef,
+ forwardRef,
+ useEffect,
+ useRef,
+} from 'react'
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from './../../ui/src/components/shadcn/ui/command'
+import { Popover, PopoverAnchor, PopoverContent } from './../../ui/src/components/shadcn/ui/popover'
+import { cn } from './../../ui/src/lib/utils/cn'
+
+const AssistantCommandsPopover = forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef & {
+ /* The children to render - this is where the AssistantChatForm should be placed */
+ children: React.ReactNode
+ /* The ref for the textarea - used with the AssistantChatForm */
+ textAreaRef: React.RefObject
+ /* The function to handle the value change */
+ setValue: (value: string) => void
+ /* The value of the textarea */
+ value: string
+ /* Whether the popover is open */
+ open: boolean
+ /* The function to handle the popover open state */
+ setOpen: (value: boolean) => void
+ /* The suggestions to display in the popover */
+ suggestions?: string[]
+ /* Whether an icon is being used in the AssistantChatForm */
+ usingIcon?: boolean
+ }
+>(
+ (
+ {
+ children,
+ textAreaRef,
+ setValue,
+ value,
+ open,
+ setOpen,
+ suggestions,
+ usingIcon = false,
+ ...props
+ },
+ ref
+ ) => {
+ const popoverContentRef = createRef()
+ const commandWidth = popoverContentRef.current?.clientWidth
+
+ const targetInputRef = useRef(null)
+
+ useEffect(() => {
+ // Attach the event listener when the component mounts
+ if (textAreaRef.current) {
+ textAreaRef.current.addEventListener('keydown', handleKeyPress)
+ }
+
+ // Detach the event listener when the component unmounts
+ return () => {
+ if (textAreaRef.current) {
+ textAreaRef.current.removeEventListener('keydown', handleKeyPress)
+ }
+ // Detach the event listener when the component unmounts
+ }
+ }, []) // Empty dependency array means this effect runs once when the component mounts
+
+ const handleKeyPress = (event: KeyboardEvent) => {
+ if (event.key === '/' && !value) {
+ // Add your action here
+ setOpen(true)
+ if (textAreaRef) textAreaRef?.current?.focus()
+ } else if (event.key === 'Escape') {
+ // Add your action here
+ setOpen(false)
+ } else if (event.key === 'Backspace') {
+ // Add your action here
+ setOpen(false)
+ } else {
+ if (event.key === 'ArrowUp' || event.key === 'ArrowDown' || event.key === 'Enter') {
+ // Forward the event to the target input
+ if (targetInputRef.current) {
+ const keyboardEvent = new KeyboardEvent('keydown', {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ key: event.key,
+ code: event.code,
+ })
+
+ targetInputRef.current.dispatchEvent(keyboardEvent)
+
+ // Schedule focus on the original input using requestAnimationFrame
+ requestAnimationFrame(() => {
+ if (textAreaRef.current) {
+ textAreaRef.current.focus()
+ }
+ })
+
+ // Prevent the default behavior for ArrowUp, ArrowDown, and Enter
+ event.preventDefault()
+ }
+ }
+ }
+ }
+
+ const resultArray = value.split(/(\s+)/).filter(Boolean)
+
+ const commands = ['fix', 'improve', 'explain', 'help']
+
+ return (
+ <>
+ {
+ setOpen(!open)
+ if (textAreaRef) textAreaRef?.current?.focus()
+ }}
+ {...props}
+ >
+
+ <>
+
+ {resultArray.map((item, i) => (
+
+ {item === ' ' ? '\u00A0' : item}
+
+ ))}
+
+ {children}
+ >
+
+ {
+ event.preventDefault()
+ }}
+ >
+
+
+
+ No results found.
+
+ {suggestions?.map((suggestion, idx) => (
+ {
+ setValue(`${suggestion}`)
+ // closing of the popover is handled by the keydown event in AssistantChatForm
+ }}
+ >
+ {suggestion}
+
+ ))}
+
+
+
+ {commands.map((command, idx) => (
+ {
+ setValue(`/${command} `)
+ // closing of the popover is handled by the keydown event in AssistantChatForm
+ }}
+ >
+ /
+ {command}
+
+ ))}
+
+
+
+
+
+ >
+ )
+ }
+)
+
+export { AssistantCommandsPopover }
diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx
index 47ecd7580bb..b87f8d0b5a4 100644
--- a/packages/ui-patterns/index.tsx
+++ b/packages/ui-patterns/index.tsx
@@ -16,3 +16,5 @@ export * from './InnerSideMenu'
export * from './ShimmeringLoader'
export * from './Dialogs/ConfirmDialog'
export * from './Dialogs/ConfirmationModal'
+export * from './AssistantChat/AssistantChatForm'
+export * from './AssistantChat/AssistantCommandsPopover'
diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx
index 92d6c1eb9f1..26e0d26ef05 100644
--- a/packages/ui/index.tsx
+++ b/packages/ui/index.tsx
@@ -120,6 +120,7 @@ export {
Popover as Popover_Shadcn_,
PopoverTrigger as PopoverTrigger_Shadcn_,
PopoverContent as PopoverContent_Shadcn_,
+ PopoverAnchor as PopoverAnchor_Shadcn_,
} from './src/components/shadcn/ui/popover'
export {
diff --git a/packages/ui/src/components/shadcn/ui/command.tsx b/packages/ui/src/components/shadcn/ui/command.tsx
index c6bee4ee407..6f72174fab5 100644
--- a/packages/ui/src/components/shadcn/ui/command.tsx
+++ b/packages/ui/src/components/shadcn/ui/command.tsx
@@ -37,9 +37,11 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = React.forwardRef<
React.ElementRef,
- React.ComponentPropsWithoutRef
->(({ className, ...props }, ref) => (
-
+ React.ComponentPropsWithoutRef
& {
+ wrapperClassName?: string
+ }
+>(({ className, wrapperClassName, ...props }, ref) => (
+