-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui): add Answer Engine Pages (#3655)
* 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
1 parent
73c3ea3
commit 9307fa0
Showing
19 changed files
with
1,974 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
Oops, something went wrong.