Chore/db design input component (#19191)

* add a new component for input

* add textArea

* some colors

* rename

* Update page.tsx

* move files

* Update ChatInput.tsx

* Update Header.tsx
This commit is contained in:
Jonathan Summers-Muir authored and GitHub committed 2023-11-24 16:15:28 +01:00
1 parent e7b6b1fe94
commit afd18c0e81
14 files changed
+206 -121

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
'use client'
import { ChatInputAtom } from '@/components/Chat/ChatInput'
import { CHAT_EXAMPLES } from '@/data/chat-examples'
import { useMutation } from '@tanstack/react-query'
import { ExternalLink } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { cn } from 'ui'
interface ChatInputParams {
userID: string | undefined
}
const suggestions = CHAT_EXAMPLES
const NewThreadInput = ({ userID }: ChatInputParams) => {
const router = useRouter()
const [value, setValue] = useState('')
const { mutate, isPending, isSuccess } = useMutation({
mutationFn: async (prompt: string) => {
const body = { prompt, userID } // Include userID in the body object
const response = await fetch('/api/ai/sql/threads/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const result = await response.json()
return result
},
onSuccess(data) {
const url = `/${data.threadId}/${data.runId}`
router.push(url)
},
})
return (
<>
<div className="relative w-10/12 xl:w-11/12 max-w-xl">
<ChatInputAtom
handleSubmit={() => {
if (value.length > 0) 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)}
/>
</div>
<div className="flex gap-3">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
className={cn(
'text-xs',
'flex items-center gap-3 !pr-3',
'transition border rounded-full px-3 py-1.5',
'text-light',
'hover:border-stronger hover:text'
)}
onClick={(event) => {
setValue(suggestion.prompt)
event.preventDefault()
}}
>
{suggestion.label}
<ExternalLink size={12} />
</button>
))}
</div>
</>
)
}
export default NewThreadInput
@@ -10,6 +10,7 @@ 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()
@@ -94,30 +95,22 @@ export const Chat = () => {
)}
<div className="px-4 pb-4">
<Input
<ChatInputAtom
value={value}
disabled={loading || inputEntered}
inputClassName="rounded-full pl-8"
loading={loading || inputEntered}
placeholder={
loading
? 'Generating reply to request...'
: 'Ask for some changes on the selected message'
}
icon={<div className="ml-1 w-2 h-2 rounded-full bg-purple-900" />}
onChange={(v) => setValue(v.target.value)}
onKeyDown={(e) => {
if (e.code === 'Enter' && value.length > 0) {
handleSubmit={() => {
if (value && value.length > 0) {
mutate(value)
setInputEntered(true)
}
}}
actions={
loading || inputEntered ? (
<div className="mr-2">
<Loader2 size={16} className="animate-spin" />
</div>
) : null
}
/>
</div>
</div>
+2 -2
View File
@@ -1,5 +1,5 @@
'use client'
import { Chat } from '@/components/Chat/Chat'
import { Chat } from '@/app/[threadId]/Chat'
import SaveSchemaDropdown from '@/components/Header/SaveSchemaDropdown'
import ToggleCodeEditorButton from '@/components/Header/ToggleCodeEditorButton'
@@ -7,7 +7,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col-reverse items-between xl:flex-row xl:items-center xl:justify-between bg-alternative h-full">
<Chat />
<div className="block xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
<div className="xl:hidden flex items-center gap-x-2 justify-end border-t py-2 px-2 bg-background">
<ToggleCodeEditorButton />
<SaveSchemaDropdown />
</div>
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import ChatInput from '@/components/Chat/ChatInput'
import { createClient } from '@/lib/supabase/server'
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import NewThreadInput from './NewThreadInput'
export const metadata: Metadata = {
title: 'database.design | Create',
@@ -17,7 +17,7 @@ const NewThread = async () => {
return (
<div className="h-full flex items-center justify-center w-full flex-col gap-y-4">
<ChatInput userID={user?.id} />
<NewThreadInput userID={user?.id} />
</div>
)
}
+1 -1
View File
@@ -7,7 +7,7 @@ const EmptyState = () => {
<div className="border rounded py-6 flex flex-col items-center justify-center gap-y-2">
<p className="text-sm text-foreground-light">No conversations created yet</p>
<Button type="default">
<Link href="/new">Start a conversation</Link>
<Link href="/">Start a conversation</Link>
</Button>
</div>
)
+68 -94
View File
@@ -1,111 +1,85 @@
'use client'
import { useMutation } from '@tanstack/react-query'
import { Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { createRef, forwardRef, useEffect } from 'react'
import { TextArea_Shadcn_, cn } from 'ui'
import { Input } from 'ui'
interface ChatInputParams {
userID: string | undefined
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
loading: boolean
handleSubmit: () => void
}
const suggestions = [
{ label: 'Twitter clone', prompt: 'Create a twitter clone' },
{
label: 'Chat application',
prompt:
'Create a chat application that supports sending messages either through channels or directly between users',
},
{
label: 'User management',
prompt: 'Create a simple user management schema that supports role based access control',
},
{ label: 'To do list', prompt: 'Create a simple schema for me to track a to do list' },
]
const ChatInputAtom = ({ className, loading, handleSubmit, ...props }: TextAreaProps) => {
const ref = createRef<HTMLTextAreaElement>()
const ChatInput = ({ userID }: ChatInputParams) => {
const router = useRouter()
const [value, setValue] = useState('')
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])
const { mutate, isPending, isSuccess } = useMutation({
mutationFn: async (prompt: string) => {
const body = { prompt, userID } // Include userID in the body object
useEffect(() => {
ref?.current?.focus()
}, [props.value, ref])
const response = await fetch('/api/ai/sql/threads/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const result = await response.json()
return result
},
onSuccess(data) {
const url = `/${data.threadId}/${data.runId}`
router.push(url)
},
})
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 (
<>
<div className="flex items-center gap-x-1.5 font-mono font-bold text-xl">
<span>database</span>
<div className="w-1.5 h-1.5 rounded-full bg-purple-900"></div>
<span>design</span>
</div>
<Input
<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
size="large"
value={value}
className="w-10/12 xl:w-11/12 max-w-xl shadow"
inputClassName="rounded-full text-sm pr-10"
placeholder="e.g Create a Telegram-like chat application"
disabled={isPending || isSuccess}
onKeyDown={(e) => {
if (e.code === 'Enter') {
if (value.length > 0) mutate(value)
}
}}
onChange={(e) => setValue(e.target.value)}
actions={
isPending || isSuccess ? (
<div className="mr-1">
<Loader2 size={22} className="animate-spin" />
</div>
) : (
<div className="flex items-center justify-center w-7 h-7 bg-surface-300 border border-control rounded-full mr-0.5 p-1.5">
<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>
)
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="flex items-center space-x-2">
{suggestions.map((suggestion, idx) => (
<button
key={idx}
className="border rounded-full px-4 py-2"
onClick={() => setValue(suggestion.prompt)}
<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'
)}
>
<p className="text-xs">{suggestion.label}</p>
</button>
))}
<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>
)
}
export default ChatInput
ChatInputAtom.displayName = 'ChatInputAtom'
export { ChatInputAtom }
@@ -21,7 +21,7 @@ const Header = async () => {
<Link href="/">
<div className="flex items-center gap-x-1.5 font-mono">
<span>database</span>
<div className="w-1.5 h-1.5 rounded-full bg-purple-900"></div>
<div className="w-1.5 h-1.5 rounded-full bg-dbnew"></div>
<span>design</span>
</div>
</Link>
@@ -44,7 +44,7 @@ const HeaderActions = ({ user }: HeaderActionsProps) => {
)}
<Button type="default" className="hidden xl:block">
<Link href="/new">New conversation</Link>
<Link href="/">New conversation</Link>
</Button>
{currentUser ? (
+15
View File
@@ -0,0 +1,15 @@
const CHAT_EXAMPLES = [
{ label: 'Twitter clone', prompt: 'Create a twitter clone' },
{
label: 'Chat application',
prompt:
'Create a chat application that supports sending messages either through channels or directly between users',
},
{
label: 'User management',
prompt: 'Create a simple user management schema that supports role based access control',
},
{ label: 'To do list', prompt: 'Create a simple schema for me to track a to do list' },
]
export { CHAT_EXAMPLES }
-7
View File
@@ -19,13 +19,6 @@ const nextConfig = {
})
return config
},
redirects: () => [
{
source: '/',
destination: '/new',
permanent: true,
},
],
}
module.exports = nextConfig
+1
View File
@@ -144,6 +144,7 @@ export {
} from './src/components/shadcn/ui/accordion'
export { Input as Input_Shadcn_ } from './src/components/shadcn/ui/input'
export { TextArea as TextArea_Shadcn_ } from './src/components/shadcn/ui/text-area'
export { Label as Label_Shadcn_ } from './src/components/shadcn/ui/label'
export * from './src/components/shadcn/ui/switch'
export { Checkbox as Checkbox_Shadcn_ } from './src/components/shadcn/ui/checkbox'
@@ -0,0 +1,28 @@
import * as React from 'react'
import { cn } from '@ui/lib/utils'
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const customClasses = ['bg-control']
const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-10 w-full rounded-md border border-control bg-control px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50',
...customClasses,
className
)}
ref={ref}
{...props}
/>
)
}
)
TextArea.displayName = 'Input'
export { TextArea }