mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
1 parent
e7b6b1fe94
commit
afd18c0e81
14 files changed
+206
-121
No files matched your search
@@ -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
|
||||
File renamed without changes.
+5
-12
@@ -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>
|
||||
File renamed without changes.
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 }
|
||||
@@ -19,13 +19,6 @@ const nextConfig = {
|
||||
})
|
||||
return config
|
||||
},
|
||||
redirects: () => [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/new',
|
||||
permanent: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -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 }
|
||||
Reference in new issue
Block a user