Chore/server state hooks (#19203)

* add a new component for input

* add textArea

* some colors

* rename

* Update page.tsx

* move files

* Update ChatInput.tsx

* Update Header.tsx

* chore:upgrade next14

* Update package-lock.json

* update graphql

* add graphql dep

* Update package.json

* Update package-lock.json

* added server action in

* more actions

* getting actions in everywhere

* Update ConfirmDeleteThreadModal.tsx

* Update Input.tsx

* update modal action

* Bump nextjs in database-new to v14.

* fix build

* trying server hooks

* more experiemntal stuff

* update delete

* shimmering

* update thread UI

* Update actions.ts

* Update actions.ts

* Update Thread.tsx

* Update Input.tsx

* updated

* delete thread updated

* fix build

* Update package-lock.json

* Update edit thread modal

* fixing build

* reset

* Update package-lock.json

* Update SpreadsheetImport.utils.ts

* reset

* Update package-lock.json

* Update sitemap_www.xml

* pacakge update

* change to node 18

* Fix deleting threads

---------

Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
Jonathan Summers-MuirandTerry Sutton authored and GitHub committed 2023-11-28 12:35:44 -03:30
1 parent 356f1ef654
commit fe053fffb1
12 files changed
+289 -82

No files matched your search

+58 -14
View File
@@ -6,6 +6,7 @@ import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import OpenAI from 'openai'
import { z } from 'zod'
const openai = new OpenAI()
@@ -24,30 +25,73 @@ export async function logout() {
redirect('/')
}
export async function deleteThread(threadID: string) {
export async function deleteThread(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
await supabase.from('threads').delete().eq('thread_id', threadID)
} catch (error) {
if (error) console.error('Error deleting thread: ', error)
const schema = z.object({
thread_id: z.string(),
})
const data = schema.parse({
thread_id: formData.get('thread_id'),
})
await supabase.from('threads').delete().eq('thread_id', data.thread_id)
await openai.beta.threads.del(data.thread_id)
revalidatePath('/profile')
return {
success: true,
data,
}
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to delete the thread',
data: undefined,
}
}
await openai.beta.threads.del(threadID)
revalidatePath('/profile')
}
export async function updateThreadName(threadId: number, threadName: string) {
export async function updateThreadName(prevState: any, formData: FormData) {
const cookieStore = cookies()
const supabase = createClient(cookieStore)
try {
await supabase.from('threads').update({ thread_title: threadName }).eq('id', threadId)
} catch (error) {
if (error) console.error('Error updating thread: ', error)
}
const schema = z.object({
thread_title: z.string(),
row_id: z.string(),
})
revalidatePath('/profile')
const data = schema.parse({
thread_title: formData.get('thread_title'),
row_id: formData.get('row_id'),
})
const { error } = await supabase
.from('threads')
.update({ thread_title: data.thread_title })
.eq('id', data.row_id)
if (error) {
throw error
}
revalidatePath('/profile')
return {
message: 'Thread name updated to ' + data.thread_title,
success: true,
data,
}
} catch (error: any) {
console.error(error)
return {
success: false,
message: 'Failed to update title to update title',
data: undefined,
}
}
}
+31
View File
@@ -140,3 +140,34 @@ main {
}
/* .monaco-editor .overflow-guard */
.shimmering-loader {
animation: shimmer 2s infinite linear;
background: linear-gradient(
to right,
hsl(var(--border-default)) 4%,
hsl(var(--background-surface-100)) 25%,
hsl(var(--border-default)) 36%
);
background-size: 1000px 100%;
}
.dark .shimmering-loader {
animation: shimmer 2s infinite linear;
background: linear-gradient(
to right,
hsl(var(--background-surface-100)) 4%,
hsl(var(--border-default)) 25%,
hsl(var(--background-surface-100)) 36%
);
background-size: 1000px 100%;
}
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
@@ -1,8 +1,10 @@
'use client'
import { Button, Modal } from 'ui'
import { useEffect, createRef } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { Button, Input_Shadcn_, Modal } from 'ui'
import { deleteThread } from '@/app/actions'
import { ThreadType } from './Threads'
import { deleteThread } from '../actions'
const ConfirmDeleteThreadModal = ({
thread,
@@ -13,6 +15,46 @@ const ConfirmDeleteThreadModal = ({
onClose: () => void
visible: boolean
}) => {
const formRef = createRef<HTMLFormElement>()
const initialState = {
message: '',
success: false,
data: {
thread_id: thread.thread_id,
},
}
const [state, formAction] = useFormState(deleteThread, initialState)
useEffect(() => {
if (state?.success === true) {
onClose()
formRef.current?.reset()
state.success = false
}
}, [state, onClose, formRef])
useEffect(() => {
if (state?.success === true) {
onClose()
formRef.current?.reset()
state.success = false
}
}, [state, onClose, formRef])
function SubmitButton() {
const { pending } = useFormStatus()
return (
<Button type="warning" htmlType="submit" aria-disabled={pending} loading={pending}>
Delete thread
</Button>
)
}
console.log('thread', thread)
return (
<Modal
size="small"
@@ -22,16 +64,15 @@ const ConfirmDeleteThreadModal = ({
header="Confirm to delete thread?"
className="pb-2"
>
<form action={() => deleteThread(thread.thread_id)}>
<form action={formAction} key={`${thread.id}-delete-thread-form`}>
<Modal.Content className="py-4">
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
</Modal.Content>
<Modal.Separator />
<Input_Shadcn_ name="thread_id" required type="hidden" value={thread.thread_id} />
<Modal.Content className="flex flex-row gap-3 justify-end">
<Button type="default">Cancel</Button>
<Button type="warning" htmlType="submit">
Delete thread
</Button>
<SubmitButton />
</Modal.Content>
</form>
</Modal>
@@ -1,10 +1,10 @@
'use client'
import { Button, Input, Modal } from 'ui'
import { updateThreadName } from '@/app/actions'
import { createRef, useEffect } from 'react'
import { useFormState, useFormStatus } from 'react-dom'
import { Button, Input_Shadcn_, Label_Shadcn_, Modal } from 'ui'
import { ThreadType } from './Threads'
import { useState } from 'react'
import { updateThreadName } from '../actions'
const EditThreadModal = ({
thread,
@@ -15,7 +15,36 @@ const EditThreadModal = ({
onClose: () => void
visible: boolean
}) => {
const [value, setValue] = useState(thread.thread_title)
const formRef = createRef<HTMLFormElement>()
const initialState = {
message: undefined,
success: undefined,
data: {
row_id: thread.id,
thread_title: thread.thread_title,
},
}
const [state, formAction] = useFormState(updateThreadName, initialState)
useEffect(() => {
if (state?.success === true) {
onClose()
formRef.current?.reset()
state.success = undefined
}
}, [state, onClose, formRef])
function SubmitButton() {
const { pending } = useFormStatus()
return (
<Button type="secondary" htmlType="submit" aria-disabled={pending} loading={pending}>
Update thread name
</Button>
)
}
return (
<Modal
@@ -27,31 +56,24 @@ const EditThreadModal = ({
header="Edit thread name"
className="pb-2"
>
<form
action={(formData: FormData) => {
const threadNameEntry: FormDataEntryValue | null = formData.get('threadName')
// Check if threadNameEntry is not null and is of type string
if (threadNameEntry !== null && typeof threadNameEntry === 'string') {
const threadName: string = threadNameEntry
updateThreadName(thread.id, threadName)
}
}}
>
<form ref={formRef} action={formAction} key={`${thread.id}-update-thread-title-form`}>
<Modal.Content className="py-4">
<Input
label="Provide a name for your thread"
value={value}
name="threadName"
onChange={(e) => setValue(e.target.value)}
<Label_Shadcn_ htmlFor="thread_title">Provide a name for your thread</Label_Shadcn_>
<Input_Shadcn_
placeholder="Type in a name for the thread..."
type="text"
name="thread_title"
defaultValue={state.data.thread_title}
/>
<Input_Shadcn_ name="row_id" value={state.data.row_id} type="hidden" />
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex flex-row gap-3 justify-end">
<Button type="default">Cancel</Button>
<Button type="secondary" htmlType="submit">
Update thread name
</Button>
<SubmitButton />
<p aria-live="polite" className="sr-only" role="status">
{state?.message}
</p>
</Modal.Content>
</form>
</Modal>
@@ -1,4 +1,5 @@
'use client'
import Link from 'next/link'
import { Button } from 'ui'
+37 -25
View File
@@ -2,8 +2,8 @@
import { timeAgo } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
@@ -15,10 +15,9 @@ import {
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
import EditThreadModal from './EditThreadModal'
import { ThreadType } from './Threads'
import { useState } from 'react'
const Thread = ({ thread }: { thread: ThreadType }) => {
const formattedTimeAgo = timeAgo(thread.created_at)
const formattedTimeAgo = timeAgo(thread.modified_at)
const [editOpen, setEditOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
@@ -27,32 +26,45 @@ const Thread = ({ thread }: { thread: ThreadType }) => {
<>
<div
key={thread.id}
className="group flex items-center justify-between border rounded w-full px-4 py-2 transition bg-surface-100 hover:bg-surface-200"
className="group w-full flex flex-row items-center gap-6 border rounded pl-5 pr-2 transition bg-surface-100 hover:bg-surface-200 h-14"
>
<div className="flex flex-col gap-y-1">
<Link className="text-sm hover:underline" href={`/${thread.thread_id}/${thread.run_id}`}>
{thread.thread_title}
<div className="flex flex-col grow overflow-hidden">
<Link
className="flex text-sm group-hover:underline truncate"
href={`/${thread.thread_id}/${thread.run_id}`}
>
<span className="truncate">{thread.thread_title}</span>
</Link>
<span className="text-xs text-foreground-light">Last updated {formattedTimeAgo}</span>
<span className="text-xs text-foreground-lighter font-mono">
Last updated {formattedTimeAgo}
</span>
</div>
<div className="flex shrink">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="px-1 group">
<IconMoreVertical size={14} className="text-light group-hover:text" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32" align="end">
<DropdownMenuItem
className="space-x-2"
onClick={() => {
console.log('trying to edit thread')
setEditOpen(true)
}}
>
<IconEdit2 size={14} />
<p>Edit name</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
<IconTrash2 size={14} />
<p>Delete thread</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-32" align="end">
<DropdownMenuItem className="space-x-2" onClick={() => setEditOpen(true)}>
<IconEdit2 size={14} />
<p>Edit name</p>
</DropdownMenuItem>
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
<IconTrash2 size={14} />
<p>Delete thread</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<ConfirmDeleteThreadModal
key={`${thread.id}-delete-dialog`}
thread={thread}
+1 -1
View File
@@ -26,7 +26,7 @@ async function Threads() {
return (
<div className="flex flex-col gap-y-3">
{threads.length > 0 ? (
threads.map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
threads.sort().map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
) : (
<EmptyState />
)}
+13 -1
View File
@@ -4,6 +4,7 @@ import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import Threads from './Threads'
import { Suspense } from 'react'
import ShimmeringLoader from '@/components/Shimmer'
import type { Metadata } from 'next'
dayjs.extend(relativeTime)
@@ -45,7 +46,18 @@ const Profile = async () => {
<div className="w-full h-px border-t" />
<Suspense fallback={<div>Loading...</div>}>
<Suspense
fallback={
<div className="flex flex-col gap-4">
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-50" />
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-25" />
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-10" />
</div>
}
>
<Threads />
</Suspense>
</div>
+24
View File
@@ -0,0 +1,24 @@
const ANIMATION_DELAY = 150
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => {
return (
<div
className={`shimmering-loader rounded py-3 ${className}`}
style={{
animationFillMode: 'backwards',
animationDelay: `${delayIndex * animationDelay}ms`,
}}
/>
)
}
const GenericSkeletonLoader = () => (
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-3/4" />
<ShimmeringLoader className="w-1/2" />
</div>
)
export { GenericSkeletonLoader }
export default ShimmeringLoader
+3 -2
View File
@@ -20,14 +20,15 @@
"eslint-config-next": "^14.0.3",
"lodash": "^4.17.21",
"lucide-react": "^0.292.0",
"next": "^14.0.3",
"openai": "^4.17.0",
"next": "^14.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-flow": "^1.0.3",
"sql-formatter": "^13.1.0",
"ui": "*",
"valtio": "^1.12.0"
"valtio": "^1.12.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^18.11.9",
+14
View File
@@ -0,0 +1,14 @@
import 'react-dom'
declare module 'react-dom' {
function useFormState<State>(
action: (state: State) => Promise<State>,
initialState: State,
permalink?: string
): [state: State, dispatch: () => void]
function useFormState<State, Payload>(
action: (state: State, payload: Payload) => Promise<State>,
initialState: State,
permalink?: string
): [state: State, dispatch: (payload: Payload) => void]
}
+14 -9
View File
@@ -49,7 +49,8 @@
"react-flow": "^1.0.3",
"sql-formatter": "^13.1.0",
"ui": "*",
"valtio": "^1.12.0"
"valtio": "^1.12.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^18.11.9",
@@ -242,8 +243,9 @@
}
},
"apps/database-new/node_modules/@types/node": {
"version": "18.18.9",
"license": "MIT",
"version": "18.18.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.13.tgz",
"integrity": "sha512-vXYZGRrSCreZmq1rEjMRLXJhiy8MrIeVasx+PCVlP414N7CJLHnMf+juVvjdprHyH+XRy3zKZLHeNueOpJCn0g==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -11152,8 +11154,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.2.37",
"license": "MIT",
"version": "18.2.39",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@@ -11196,9 +11199,10 @@
}
},
"node_modules/@types/react-dom": {
"version": "18.2.8",
"version": "18.2.17",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
@@ -36007,8 +36011,9 @@
"license": "MIT"
},
"node_modules/zod": {
"version": "3.22.3",
"license": "MIT",
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}