chore: added pattern UI for assistant input (#19294)

* chore: added pattern for input

* migrated whole form over

* Update packages/ui/src/patterns/AssistantChatForm.tsx

* Update packages/ui/src/patterns/AssistantChatForm.tsx

* Update packages/ui/src/patterns/AssistantChatForm.tsx

---------

Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
Jonathan Summers-MuirandTerry Sutton authored and GitHub committed 2023-11-30 14:22:39 -03:30
1 parent a51cc06f41
commit 5ef9784506
6 files changed
+136 -128

No files matched your search

+17 -17
View File
@@ -1,22 +1,17 @@
'use client'
import { ChatInputAtom } from '@/components/Chat/ChatInput'
import { CHAT_EXAMPLES } from '@/data/chat-examples'
import { useAppStateSnapshot } from '@/lib/state'
import { createClient } from '@/lib/supabase/client'
import { useMutation } from '@tanstack/react-query'
import { ExternalLink } from 'lucide-react'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
import { cn } from 'ui'
interface ChatInputParams {
userID: string | undefined
}
import { useRouter } from 'next/navigation'
import { createRef, useState } from 'react'
import { AssistantChatForm, cn } from 'ui'
const suggestions = CHAT_EXAMPLES
const NewThreadInput = ({ userID }: ChatInputParams) => {
const NewThreadInput = () => {
const router = useRouter()
const [value, setValue] = useState(() => {
@@ -36,7 +31,11 @@ const NewThreadInput = ({ userID }: ChatInputParams) => {
const { mutate, isPending, isSuccess } = useMutation({
mutationFn: async (prompt: string) => {
const body = { prompt, userID } // Include userID in the body object
const {
data: { user },
} = await supabase.auth.getUser()
const body = { prompt, userID: user?.id } // Include userID in the body object
const response = await fetch('/api/ai/sql/threads/create', {
method: 'POST',
@@ -55,8 +54,12 @@ const NewThreadInput = ({ userID }: ChatInputParams) => {
return (
<>
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
<ChatInputAtom
handleSubmit={async () => {
<AssistantChatForm
key={'new-thread-form'}
id={'new-thread-form'}
onSubmit={async (event) => {
event.preventDefault()
const {
data: { user },
} = await supabase.auth.getUser()
@@ -70,21 +73,18 @@ const NewThreadInput = ({ userID }: ChatInputParams) => {
mutate(value)
}
}}
autoFocus
value={value}
rows={1}
contentEditable
aria-expanded={false}
placeholder="e.g Create a Telegram-like chat application"
disabled={isPending || isSuccess}
loading={isPending || isSuccess}
onChange={(e) => setValue(e.target.value)}
onValueChange={(e) => setValue(e.target.value)}
/>
</div>
<div className="flex gap-3">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
type="button"
className={cn(
'text-xs',
'flex items-center gap-3 !pr-3',
+11 -11
View File
@@ -1,16 +1,16 @@
'use client'
import { useMutation } from '@tanstack/react-query'
import { sortBy } from 'lodash'
import { Loader2 } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { useEffect, useMemo, useState } from 'react'
import { Input, ScrollArea, cn } from 'ui'
import { FormEventHandler, useEffect, useMemo, useState } from 'react'
import { AssistantChatForm, ScrollArea, cn } from 'ui'
import { useMessagesQuery } from '@/data/messages-query'
import { AssistantMessage, UserMessage } from '@/lib/types'
import BottomMarker from './BottomMarker'
import UserChat from './UserChat'
import { ChatInputAtom } from '@/components/Chat/ChatInput'
export const Chat = () => {
const router = useRouter()
@@ -96,7 +96,13 @@ export const Chat = () => {
)}
<div className="px-4 pb-4">
<ChatInputAtom
<AssistantChatForm
key={`chat-thread-form-${runId}`}
id={`chat-thread-form-${runId}`}
onSubmit={async (event) => {
event.preventDefault()
mutate(value)
}}
value={value}
disabled={loading || inputEntered}
loading={loading || inputEntered}
@@ -105,13 +111,7 @@ export const Chat = () => {
? 'Generating reply to request...'
: 'Ask for some changes on the selected message'
}
onChange={(v) => setValue(v.target.value)}
handleSubmit={() => {
if (value && value.length > 0) {
mutate(value)
setInputEntered(true)
}
}}
onValueChange={(v) => setValue(v.target.value)}
/>
</div>
</div>
+4 -15
View File
@@ -1,9 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import NewThreadInput from './NewThreadInput'
import { LoginDialog } from './LoginDialog'
import LoginForm from '@/components/Auth/LoginForm'
import type { Metadata } from 'next'
import { LoginDialog } from './LoginDialog'
import NewThreadInput from './NewThreadInput'
export const metadata: Metadata = {
title: 'database.design | Create',
@@ -14,19 +12,10 @@ const NewThread = async ({
}: {
searchParams: { prompt: string; message: string }
}) => {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
const {
data: { user },
} = await supabase.auth.getUser()
console.log('searchParams on page level', searchParams)
return (
<>
<div className="h-full flex items-center justify-center w-full flex-col gap-y-4">
<NewThreadInput userID={user?.id} />
<NewThreadInput />
</div>
<LoginDialog>
<LoginForm searchParams={searchParams} />
@@ -1,85 +0,0 @@
import { Loader2 } from 'lucide-react'
import { createRef, forwardRef, useEffect } from 'react'
import { TextArea_Shadcn_, cn } from 'ui'
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
loading: boolean
handleSubmit: () => void
}
const ChatInputAtom = ({ className, loading, handleSubmit, ...props }: TextAreaProps) => {
const ref = createRef<HTMLTextAreaElement>()
useEffect(() => {
if (!props.value && ref && ref.current) {
ref.current.style.height = '40px'
} else if (ref && ref.current) {
console.log('ref.current.scrollHeight', ref.current.scrollHeight)
const newHeight = ref.current.scrollHeight + 'px'
console.log('new height', newHeight)
ref.current.style.height = newHeight
}
}, [props.value, ref])
useEffect(() => {
ref?.current?.focus()
}, [props.value, ref])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Check if the pressed key is "Enter" (key code 13)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
// Manually submit the form
// value is handled by parent component
handleSubmit()
}
}
return (
<form className="relative">
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
<TextArea_Shadcn_
autoFocus
className={
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
}
ref={ref}
spellCheck={false}
onKeyDown={handleKeyDown}
{...props}
/>
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
{loading ? (
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
) : (
<div
className={cn(
'transition-all',
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
!props.value ? 'text-muted opacity-50' : 'text-default opacity-100'
)}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
fill="currentColor"
></path>
</svg>
</div>
)}
</div>
</form>
)
}
ChatInputAtom.displayName = 'ChatInputAtom'
export { ChatInputAtom }
+4
View File
@@ -179,6 +179,10 @@ export { ScrollArea, ScrollBar } from './src/components/shadcn/ui/scroll-area'
export { Separator } from './src/components/shadcn/ui/separator'
// patterns
export { AssistantChatForm } from './src/patterns/AssistantChatForm'
// links
export * from './src/components/TextLink'
@@ -0,0 +1,100 @@
import { Loader2 } from 'lucide-react'
import React, { ChangeEvent, createRef, useEffect } from 'react'
import { TextArea } from '../components/shadcn/ui/text-area'
import { cn } from '../lib/utils'
export interface FormProps extends React.FormHTMLAttributes<HTMLFormElement> {
loading: boolean
disabled?: boolean
value?: string
onValueChange: (value: ChangeEvent<HTMLTextAreaElement>) => void
}
const AssistantChatForm = React.forwardRef<HTMLFormElement, FormProps>(
({ loading, disabled, value, onValueChange, ...props }, ref) => {
const textAreaRef = createRef<HTMLTextAreaElement>()
const submitRef = createRef<HTMLButtonElement>()
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])
useEffect(() => {
textAreaRef?.current?.focus()
}, [value, textAreaRef])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
// Check if the pressed key is "Enter" (key code 13) without the "Shift" key
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
if (submitRef.current) {
submitRef.current.click()
}
}
}
return (
<form ref={ref} className="relative" {...props}>
<div className={cn('absolute', 'top-2 left-2', 'ml-1 w-6 h-6 rounded-full bg-dbnew')}></div>
<TextArea
ref={textAreaRef}
autoFocus
rows={1}
disabled={disabled}
contentEditable
aria-expanded={false}
className={
'transition-all text-sm pl-12 pr-10 rounded-[18px] resize-none box-border leading-6'
}
placeholder={props.placeholder}
spellCheck={false}
value={value}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(event)}
onKeyDown={handleKeyDown}
/>
<div className="absolute right-1.5 top-1.5 flex gap-3 items-center">
{loading && (
<Loader2 size={22} className="animate-spin w-7 h-7 text-muted" strokeWidth={1} />
)}
<button
ref={submitRef}
type="submit"
className={cn(
'transition-all',
'flex items-center justify-center w-7 h-7 border border-control rounded-full mr-0.5 p-1.5 background-alternative',
!value ? 'text-muted opacity-50' : 'text-default opacity-100',
loading && 'hidden'
)}
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.5 3V2.25H15V3V10C15 10.5523 14.5522 11 14 11H3.56062L5.53029 12.9697L6.06062 13.5L4.99996 14.5607L4.46963 14.0303L1.39641 10.9571C1.00588 10.5666 1.00588 9.93342 1.39641 9.54289L4.46963 6.46967L4.99996 5.93934L6.06062 7L5.53029 7.53033L3.56062 9.5H13.5V3Z"
fill="currentColor"
></path>
</svg>
</button>
</div>
</form>
)
}
)
AssistantChatForm.displayName = 'AssistantChatForm'
export { AssistantChatForm }