diff --git a/ee/tabby-ui/app/not-found.tsx b/ee/tabby-ui/app/not-found.tsx new file mode 100644 index 000000000000..c93bc7a92a0a --- /dev/null +++ b/ee/tabby-ui/app/not-found.tsx @@ -0,0 +1,5 @@ +import NotFoundPage from '@/components/not-found-page' + +export default function NotFound() { + return +} diff --git a/ee/tabby-ui/app/search/components/header.tsx b/ee/tabby-ui/app/search/components/header.tsx new file mode 100644 index 000000000000..87404c30af0f --- /dev/null +++ b/ee/tabby-ui/app/search/components/header.tsx @@ -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) => { + e.preventDefault() + setIsDeleting(true) + deleteThread({ + id: threadIdFromURL! + }) + } + + const onNavigateToHomePage = (scroll?: boolean) => { + if (scroll) { + clearHomeScrollPosition() + } + router.push('/') + } + + return ( +
+
+ +
+
+ {streamingDone && threadIdFromURL && ( + + )} + {streamingDone && threadIdFromURL && isThreadOwner && ( + + + + + + + Delete this thread + + Are you sure you want to delete this thread? This operation is + not revertible. + + + + Cancel + + {isDeleting && ( + + )} + Yes, delete it + + + + + )} + + + + { + clearHomeScrollPosition() + }} + > + + +
+
+ ) +} diff --git a/ee/tabby-ui/app/search/components/search.tsx b/ee/tabby-ui/app/search/components/search.tsx index 4baad4b3bda3..5c65abf69ed8 100644 --- a/ee/tabby-ui/app/search/components/search.tsx +++ b/ee/tabby-ui/app/search/components/search.tsx @@ -65,7 +65,6 @@ import { import { Button, buttonVariants } from '@/components/ui/button' import { IconCheck, - IconChevronLeft, IconFileSearch, IconPlus, IconShare, @@ -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' @@ -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) @@ -719,8 +716,18 @@ export function Search() { 200 ) - if (isReady && (threadMessagesError || hasThreadError)) { - return + const style = isShowDemoBanner + ? { height: `calc(100vh - ${BANNER_HEIGHT})` } + : { height: '100vh' } + + if (isReady && (formatedThreadError || threadMessagesError)) { + return ( + + ) } if (!isReady && (isFetchingMessages || threadMessagesStale)) { @@ -739,10 +746,6 @@ export function Search() { return <> } - const style = isShowDemoBanner - ? { height: `calc(100vh - ${BANNER_HEIGHT})` } - : { height: '100vh' } - return (
@@ -931,63 +934,18 @@ 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 } - return ( -
-
- -
-
- {(streamingDone || threadIdFromURL) && ( - <> - - - )} - - - - { - clearHomeScrollPosition() - }} - > - - -
-
- ) -} - -function ThreadMessagesErrorView() { return (
@@ -995,12 +953,9 @@ function ThreadMessagesErrorView() {
-
Something went wrong
-
-
- Failed to fetch the thread, please refresh the page or start a new - thread +
{title}
+
{description}
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' diff --git a/ee/tabby-ui/components/not-found-page.tsx b/ee/tabby-ui/components/not-found-page.tsx new file mode 100644 index 000000000000..ddecf5f1f554 --- /dev/null +++ b/ee/tabby-ui/components/not-found-page.tsx @@ -0,0 +1,69 @@ +'use client' + +import Image from 'next/image' +import Link from 'next/link' +import logoDarkUrl from '@/assets/logo-dark.png' +import logoUrl from '@/assets/logo.png' + +import { cn } from '@/lib/utils' + +import { ClientOnly } from './client-only' +import { BANNER_HEIGHT, useShowDemoBanner } from './demo-banner' +import { ThemeToggle } from './theme-toggle' +import { buttonVariants } from './ui/button' +import { MyAvatar } from './user-avatar' +import UserPanel from './user-panel' + +export default function NotFoundPage() { + const [isShowDemoBanner] = useShowDemoBanner() + + const style = isShowDemoBanner + ? { + height: `calc(100vh - ${BANNER_HEIGHT})` + } + : { height: '100vh' } + + return ( +
+
+
+

+ 404 +

+

+ Oops, it looks like the page you're looking for doesn't + exist. +

+ + Home + +
+
+ ) +} + +function Header() { + return ( +
+
+ + logo + logo + +
+
+ + + + + + +
+
+ ) +} diff --git a/ee/tabby-ui/lib/tabby/gql.ts b/ee/tabby-ui/lib/tabby/gql.ts index faacd7ef063e..544d8f107eb1 100644 --- a/ee/tabby-ui/lib/tabby/gql.ts +++ b/ee/tabby-ui/lib/tabby/gql.ts @@ -23,6 +23,7 @@ import { GitRepositoriesQueryVariables, ListIntegrationsQueryVariables, ListInvitationsQueryVariables, + ListThreadsQueryVariables, SourceIdAccessPoliciesQueryVariables, UpsertUserGroupMembershipInput } from '../gql/generates/graphql' @@ -33,6 +34,7 @@ import { listInvitations, listRepositories, listSourceIdAccessPolicies, + listThreads, userGroupsQuery } from './query' import { @@ -375,6 +377,33 @@ const client = new Client({ ) }) } + }, + deleteThread(result, args, cache, info) { + if (result.deleteThread) { + cache + .inspectFields('Query') + // Update the cache within the thread-feeds only + .filter( + field => + field.fieldName === 'threads' && !field.arguments?.ids + ) + .forEach(field => { + cache.updateQuery( + { + query: listThreads, + variables: field.arguments as ListThreadsQueryVariables + }, + data => { + if (data?.threads) { + data.threads.edges = data.threads.edges.filter( + e => e.node.id !== args.id + ) + } + return data + } + ) + }) + } } } },