Skip to content

Commit

Permalink
feat(ui): add Answer Engine Pages (#3655)
Browse files Browse the repository at this point in the history
* feat(ui): add answer engine pages

* [autofix.ci] apply automated fixes

* update: context

* [autofix.ci] apply automated fixes

* feat(ui): add Answer Engine Page

* update

* update

* update

* update

* update: streaming

* update

* [autofix.ci] apply automated fixes

* update

* update: exp flag

* update: remove delay sink

* update url

* update: rename

* update

* update

* update: rename route

* update: rename route

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Feb 8, 2025
1 parent 73c3ea3 commit 9307fa0
Show file tree
Hide file tree
Showing 19 changed files with 1,974 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client'

import { useEnableDeveloperMode } from '@/lib/experiment-flags'
import { useEnableDeveloperMode, useEnablePage } from '@/lib/experiment-flags'
import { Switch } from '@/components/ui/switch'

export default function FeatureList() {
const [developerMode, toggleDeveloperMode] = useEnableDeveloperMode()
const [enablePage, toggleEnablePage] = useEnablePage()
return (
<>
{!developerMode.loading && (
Expand All @@ -23,6 +24,22 @@ export default function FeatureList() {
/>
</div>
)}
{!enablePage.loading && (
<div className="flex items-center space-x-4 rounded-md border p-4">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
{enablePage.title}
</p>
<p className="text-sm text-muted-foreground">
{enablePage.description}
</p>
</div>
<Switch
checked={enablePage.value}
onCheckedChange={toggleEnablePage}
/>
</div>
)}
</>
)
}
191 changes: 191 additions & 0 deletions ee/tabby-ui/app/pages/components/header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
'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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import {
IconChevronLeft,
IconEdit,
IconMore,
IconPlus,
IconSpinner,
IconTrash
} from '@/components/ui/icons'
import { ClientOnly } from '@/components/client-only'
import { NotificationBox } from '@/components/notification-box'
import { ThemeToggle } from '@/components/theme-toggle'
import { MyAvatar } from '@/components/user-avatar'
import UserPanel from '@/components/user-panel'

import { PageContext } from './page-context'

const deletePageMutation = graphql(/* GraphQL */ `
mutation DeletePage($id: ID!) {
deletePage(id: $id)
}
`)

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

export function Header({ pageIdFromURL, streamingDone }: HeaderProps) {
const router = useRouter()
const { isPageOwner, mode, setMode } = useContext(PageContext)
const isEditMode = mode === 'edit'
const [deleteAlertVisible, setDeleteAlertVisible] = useState(false)
const [isDeleting, setIsDeleting] = useState(false)

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

const handleDeletePage = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setIsDeleting(true)
deletePage({
id: pageIdFromURL!
})
}

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

return (
<header className="flex h-16 w-full items-center justify-between border-b 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">
{!isEditMode ? (
<>
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<IconMore />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{streamingDone && pageIdFromURL && (
<DropdownMenuItem
className="cursor-pointer gap-2"
onClick={() => onNavigateToHomePage(true)}
>
<IconPlus />
<span>Add new page</span>
</DropdownMenuItem>
)}
{streamingDone && pageIdFromURL && isPageOwner && (
<AlertDialog
open={deleteAlertVisible}
onOpenChange={setDeleteAlertVisible}
>
<AlertDialogTrigger asChild>
<DropdownMenuItem className="cursor-pointer gap-2">
<IconTrash />
Delete Page
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete this page</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this page? This
operation is not revertible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: 'destructive' })}
onClick={handleDeletePage}
>
{isDeleting && (
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
)}
Yes, delete it
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</DropdownMenuContent>
</DropdownMenu>

<Button
variant="ghost"
className="flex items-center gap-1 px-2 font-normal"
onClick={() => setMode('edit')}
>
<IconEdit />
Edit Page
</Button>
</>
) : (
<>
<Button onClick={e => setMode('view')}>Done</Button>
</>
)}
<ClientOnly>
<ThemeToggle />
</ClientOnly>
<NotificationBox className="mr-4" />
<UserPanel
showHome={false}
showSetting
beforeRouteChange={() => {
clearHomeScrollPosition()
}}
>
<MyAvatar className="h-10 w-10 border" />
</UserPanel>
</div>
</header>
)
}
13 changes: 13 additions & 0 deletions ee/tabby-ui/app/pages/components/messages-skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Skeleton } from '@/components/ui/skeleton'

export function MessagesSkeleton() {
return (
<div className="space-y-4">
<div className="space-y-2">
<Skeleton className="w-full" />
<Skeleton className="w-[70%]" />
</div>
<Skeleton className="h-40 w-full" />
</div>
)
}
64 changes: 64 additions & 0 deletions ee/tabby-ui/app/pages/components/nav-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client'

import React, { useEffect, useRef, useState } from 'react'

import { useDebounceCallback } from '@/lib/hooks/use-debounce'

import { SectionItem } from '../types'

interface Props {
sections: SectionItem[] | undefined
}

export const Navbar = ({ sections }: Props) => {
const [activeNavItem, setActiveNavItem] = useState<string | undefined>()
const observer = useRef<IntersectionObserver | null>(null)
const updateActiveNavItem = useDebounceCallback((v: string) => {
setActiveNavItem(v)
}, 200)

useEffect(() => {
const options = {
root: null,
rootMargin: '70px'
}

observer.current = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
updateActiveNavItem.run(entry.target.id)
break
}
}
}, options)

const targets = document.querySelectorAll('.section-title')
targets.forEach(target => {
observer.current?.observe(target)
})

return () => {
observer.current?.disconnect()
}
}, [])

return (
<nav className="sticky right-0 top-0 p-4">
<ul className="flex flex-col space-y-1">
{sections?.map(section => (
<li key={section.id}>
<div
className={`truncate whitespace-nowrap text-sm ${
activeNavItem === section.id
? 'text-foreground'
: 'text-muted-foreground'
}`}
>
{section.content}
</div>
</li>
))}
</ul>
</nav>
)
}
23 changes: 23 additions & 0 deletions ee/tabby-ui/app/pages/components/page-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createContext, Dispatch, SetStateAction } from 'react'

import { ExtendedCombinedError } from '@/lib/types'

type PageContextValue = {
mode: 'edit' | 'view'
setMode: Dispatch<SetStateAction<'view' | 'edit'>>
isPathnameInitialized: boolean
isLoading: boolean
onAddSection: (title: string) => void
onDeleteSection: (id: string) => void
isPageOwner: boolean
onUpdateSectionContent: (
message: string
) => Promise<ExtendedCombinedError | undefined>
pendingSectionIds: Set<string>
setPendingSectionIds: (value: SetStateAction<Set<string>>) => void
currentSectionId: string | undefined
}

export const PageContext = createContext<PageContextValue>(
{} as PageContextValue
)
Loading

0 comments on commit 9307fa0

Please sign in to comment.