Skip to content

Commit

Permalink
feat(ui): add functionality to delete entire thread (#3410)
Browse files Browse the repository at this point in the history
* feat(ui): add functionality to delete entire thread

* update: lint

* [autofix.ci] apply automated fixes

* update: handle cache

* update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Nov 13, 2024
1 parent 2d66459 commit 5c14645
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 72 deletions.
5 changes: 5 additions & 0 deletions ee/tabby-ui/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import NotFoundPage from '@/components/not-found-page'

export default function NotFound() {
return <NotFoundPage />
}
153 changes: 153 additions & 0 deletions ee/tabby-ui/app/search/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
'use client'

import { useContext, useState } from 'react'
import type { MouseEvent } from 'react'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'

import { graphql } from '@/lib/gql/generates'
import { clearHomeScrollPosition } from '@/lib/stores/scroll-store'
import { useMutation } from '@/lib/tabby/gql'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from '@/components/ui/alert-dialog'
import { Button, buttonVariants } from '@/components/ui/button'
import {
IconChevronLeft,
IconPlus,
IconSpinner,
IconTrash
} from '@/components/ui/icons'
import { ClientOnly } from '@/components/client-only'
import { ThemeToggle } from '@/components/theme-toggle'
import { MyAvatar } from '@/components/user-avatar'
import UserPanel from '@/components/user-panel'

import { SearchContext } from './search'

const deleteThreadMutation = graphql(/* GraphQL */ `
mutation DeleteThread($id: ID!) {
deleteThread(id: $id)
}
`)

type HeaderProps = {
threadIdFromURL?: string
streamingDone?: boolean
}

export function Header({ threadIdFromURL, streamingDone }: HeaderProps) {
const router = useRouter()
const { isThreadOwner } = useContext(SearchContext)
const [deleteAlertVisible, setDeleteAlertVisible] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)

const deleteThread = useMutation(deleteThreadMutation, {
onCompleted(data) {
if (data.deleteThread) {
router.replace('/')
} else {
toast.error('Failed to delete')
setIsDeleting(false)
}
},
onError(err) {
toast.error(err?.message || 'Failed to delete')
setIsDeleting(false)
}
})

const handleDeleteThread = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setIsDeleting(true)
deleteThread({
id: threadIdFromURL!
})
}

const onNavigateToHomePage = (scroll?: boolean) => {
if (scroll) {
clearHomeScrollPosition()
}
router.push('/')
}

return (
<header className="flex h-16 items-center justify-between px-4 lg:px-10">
<div className="flex items-center gap-x-6">
<Button
variant="ghost"
className="-ml-1 pl-0 text-sm text-muted-foreground"
onClick={() => onNavigateToHomePage()}
>
<IconChevronLeft className="mr-1 h-5 w-5" />
Home
</Button>
</div>
<div className="flex items-center gap-2">
{streamingDone && threadIdFromURL && (
<Button
variant="ghost"
className="flex items-center gap-1 px-2 font-normal text-muted-foreground"
onClick={() => onNavigateToHomePage(true)}
>
<IconPlus />
</Button>
)}
{streamingDone && threadIdFromURL && isThreadOwner && (
<AlertDialog
open={deleteAlertVisible}
onOpenChange={setDeleteAlertVisible}
>
<AlertDialogTrigger asChild>
<Button size="icon" variant="hover-destructive">
<IconTrash />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this thread</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this thread? This operation is
not revertible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: 'destructive' })}
onClick={handleDeleteThread}
>
{isDeleting && (
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
)}
Yes, delete it
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<ClientOnly>
<ThemeToggle className="mr-4" />
</ClientOnly>
<UserPanel
showHome={false}
showSetting
beforeRouteChange={() => {
clearHomeScrollPosition()
}}
>
<MyAvatar className="h-10 w-10 border" />
</UserPanel>
</div>
</header>
)
}
99 changes: 27 additions & 72 deletions ee/tabby-ui/app/search/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ import {
import { Button, buttonVariants } from '@/components/ui/button'
import {
IconCheck,
IconChevronLeft,
IconFileSearch,
IconPlus,
IconShare,
Expand All @@ -79,15 +78,13 @@ import {
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom'
import { ClientOnly } from '@/components/client-only'
import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner'
import NotFoundPage from '@/components/not-found-page'
import TextAreaSearch from '@/components/textarea-search'
import { ThemeToggle } from '@/components/theme-toggle'
import { MyAvatar } from '@/components/user-avatar'
import UserPanel from '@/components/user-panel'

import { AssistantMessageSection } from './assistant-message-section'
import { DevPanel } from './dev-panel'
import { Header } from './header'
import { MessagesSkeleton } from './messages-skeleton'
import { UserMessageSection } from './user-message-section'

Expand Down Expand Up @@ -707,7 +704,7 @@ export function Search() {
updateSelectedModel(model)
}

const hasThreadError = useMemo(() => {
const formatedThreadError: ExtendedCombinedError | undefined = useMemo(() => {
if (!isReady || fetchingThread || !threadIdFromURL) return undefined
if (threadError || !threadData?.threads?.edges?.length) {
return threadError || new Error(ERROR_CODE_NOT_FOUND)
Expand All @@ -719,8 +716,18 @@ export function Search() {
200
)

if (isReady && (threadMessagesError || hasThreadError)) {
return <ThreadMessagesErrorView />
const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT})` }
: { height: '100vh' }

if (isReady && (formatedThreadError || threadMessagesError)) {
return (
<ThreadMessagesErrorView
error={
(formatedThreadError || threadMessagesError) as ExtendedCombinedError
}
/>
)
}

if (!isReady && (isFetchingMessages || threadMessagesStale)) {
Expand All @@ -739,10 +746,6 @@ export function Search() {
return <></>
}

const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT})` }
: { height: '100vh' }

return (
<SearchContext.Provider
value={{
Expand All @@ -765,7 +768,7 @@ export function Search() {
<ResizablePanel>
<Header
threadIdFromURL={threadIdFromURL}
streamingDone={answer.completed}
streamingDone={!isLoading}
/>
<main className="h-[calc(100%-4rem)] pb-8 lg:pb-0">
<ScrollArea className="h-full" ref={contentContainerRef}>
Expand Down Expand Up @@ -931,76 +934,28 @@ const updateThreadMessageMutation = graphql(/* GraphQL */ `
}
`)

type HeaderProps = {
threadIdFromURL?: string
streamingDone?: boolean
interface ThreadMessagesErrorViewProps {
error: ExtendedCombinedError
}
function ThreadMessagesErrorView({ error }: ThreadMessagesErrorViewProps) {
let title = 'Something went wrong'
let description =
'Failed to fetch the thread, please refresh the page or start a new thread'

function Header({ threadIdFromURL, streamingDone }: HeaderProps) {
const router = useRouter()

const onNavigateToHomePage = (scroll?: boolean) => {
if (scroll) {
clearHomeScrollPosition()
}
router.push('/')
if (error.message === ERROR_CODE_NOT_FOUND) {
return <NotFoundPage />
}

return (
<header className="flex h-16 items-center justify-between px-4 lg:px-10">
<div className="flex items-center gap-x-6">
<Button
variant="ghost"
className="-ml-1 pl-0 text-sm text-muted-foreground"
onClick={() => onNavigateToHomePage()}
>
<IconChevronLeft className="mr-1 h-5 w-5" />
Home
</Button>
</div>
<div className="flex items-center gap-2">
{(streamingDone || threadIdFromURL) && (
<>
<Button
variant="ghost"
className="flex items-center gap-1 px-2 font-normal text-muted-foreground"
onClick={() => onNavigateToHomePage(true)}
>
<IconPlus />
</Button>
</>
)}
<ClientOnly>
<ThemeToggle className="mr-4" />
</ClientOnly>
<UserPanel
showHome={false}
showSetting
beforeRouteChange={() => {
clearHomeScrollPosition()
}}
>
<MyAvatar className="h-10 w-10 border" />
</UserPanel>
</div>
</header>
)
}

function ThreadMessagesErrorView() {
return (
<div className="flex h-screen flex-col">
<Header />
<div className="flex-1">
<div className="flex h-full flex-col items-center justify-center gap-2">
<div className="flex items-center gap-2">
<IconFileSearch className="h-6 w-6" />
<div className="text-xl font-semibold">Something went wrong</div>
</div>
<div>
Failed to fetch the thread, please refresh the page or start a new
thread
<div className="text-xl font-semibold">{title}</div>
</div>
<div>{description}</div>
<Link
href="/"
onClick={clearHomeScrollPosition}
Expand Down Expand Up @@ -1091,7 +1046,7 @@ function formatThreadRunErrorMessage(error?: ExtendedCombinedError) {
if (
some(error.graphQLErrors, o => o.extensions?.code === ERROR_CODE_NOT_FOUND)
) {
return `The thread has expired`
return `The thread has expired or does not exist.`
}

return error.message || 'Failed to fetch'
Expand Down
Loading

0 comments on commit 5c14645

Please sign in to comment.