mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
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:
1 parent
a51cc06f41
commit
5ef9784506
6 files changed
+136
-128
No files matched your search
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user