From 042adfd349d98488b71fc2294527745643cb07c4 Mon Sep 17 00:00:00 2001 From: Wang Zixiao Date: Fri, 7 Jun 2024 14:13:46 +0800 Subject: [PATCH] feat(ui): add new search page to support chat search functionality (#2144) * feat(ui): searching function * wip * rendering * trigger request ux * copy and regenerate * experiment flag & textare height * [autofix.ci] apply automated fixes * update source & related ux * citation * update * update * update * cleaning fixme * update * update * update dialog component * update --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../experiments/components/feature-list.tsx | 17 +- ee/tabby-ui/app/(home)/page.tsx | 41 +- ee/tabby-ui/app/globals.css | 4 + ee/tabby-ui/app/search/components/search.css | 18 + ee/tabby-ui/app/search/components/search.tsx | 746 ++++++++++++++++++ ee/tabby-ui/app/search/page.tsx | 11 + ee/tabby-ui/components/chat/chat.tsx | 2 +- ee/tabby-ui/components/copy-button.tsx | 7 +- ee/tabby-ui/components/textarea-search.tsx | 83 ++ ee/tabby-ui/components/ui/dialog.tsx | 26 +- ee/tabby-ui/components/ui/icons.tsx | 46 +- ee/tabby-ui/components/ui/sheet.tsx | 66 +- ee/tabby-ui/lib/constants/index.ts | 3 +- ee/tabby-ui/lib/experiment-flags.ts | 10 +- .../chat => lib/hooks}/use-tabby-answer.ts | 6 +- ee/tabby-ui/package.json | 4 +- pnpm-lock.yaml | 6 +- 17 files changed, 1041 insertions(+), 55 deletions(-) create mode 100644 ee/tabby-ui/app/search/components/search.css create mode 100644 ee/tabby-ui/app/search/components/search.tsx create mode 100644 ee/tabby-ui/app/search/page.tsx create mode 100644 ee/tabby-ui/components/textarea-search.tsx rename ee/tabby-ui/{components/chat => lib/hooks}/use-tabby-answer.ts (94%) diff --git a/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx b/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx index 1d0ac663f672..5c9b89a4fa11 100644 --- a/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx +++ b/ee/tabby-ui/app/(dashboard)/experiments/components/feature-list.tsx @@ -1,11 +1,15 @@ 'use client' -import { useEnableCodeBrowserQuickActionBar } from '@/lib/experiment-flags' +import { + useEnableCodeBrowserQuickActionBar, + useEnableSearch +} from '@/lib/experiment-flags' import { Switch } from '@/components/ui/switch' export default function FeatureList() { const [quickActionBar, toggleQuickActionBar] = useEnableCodeBrowserQuickActionBar() + const [search, toggleSearch] = useEnableSearch() return ( <> {!quickActionBar.loading && ( @@ -24,6 +28,17 @@ export default function FeatureList() { /> )} + {!search.loading && ( +
+
+

{search.title}

+

+ {search.description} +

+
+ +
+ )} ) } diff --git a/ee/tabby-ui/app/(home)/page.tsx b/ee/tabby-ui/app/(home)/page.tsx index e25826443cb0..1c52d9227d71 100644 --- a/ee/tabby-ui/app/(home)/page.tsx +++ b/ee/tabby-ui/app/(home)/page.tsx @@ -1,10 +1,14 @@ 'use client' import { useState } from 'react' +import Image from 'next/image' import Link from 'next/link' import { useRouter } from 'next/navigation' +import logoUrl from '@/assets/tabby.png' import { noop } from 'lodash-es' +import { SESSION_STORAGE_KEY } from '@/lib/constants' +import { useEnableSearch } from '@/lib/experiment-flags' import { graphql } from '@/lib/gql/generates' import { useHealth } from '@/lib/hooks/use-health' import { useMe } from '@/lib/hooks/use-me' @@ -14,6 +18,7 @@ import { useSignOut } from '@/lib/tabby/auth' import { useMutation } from '@/lib/tabby/gql' import { Button } from '@/components/ui/button' import { CardContent, CardFooter } from '@/components/ui/card' +import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog' import { IconChat, IconCode, @@ -22,6 +27,7 @@ import { IconLogout, IconMail, IconRotate, + IconSearch, IconSpinner, IconUser, IconVSCode @@ -36,6 +42,7 @@ import { } from '@/components/ui/tooltip' import { CopyButton } from '@/components/copy-button' import SlackDialog from '@/components/slack-dialog' +import TextAreaSearch from '@/components/textarea-search' import { ThemeToggle } from '@/components/theme-toggle' import { UserAvatar } from '@/components/user-avatar' @@ -154,11 +161,13 @@ function IDELink({ } function MainPanel() { + const [searchFlag] = useEnableSearch() const { data: healthInfo } = useHealth() const [{ data }] = useMe() const isChatEnabled = useIsChatEnabled() const signOut = useSignOut() const [signOutLoading, setSignOutLoading] = useState(false) + const [isSearchOpen, setIsSearchOpen] = useState(false) if (!healthInfo || !data?.me) return <> @@ -168,8 +177,15 @@ function MainPanel() { await signOut() setSignOutLoading(false) } + + const onSearch = (question: string) => { + sessionStorage.setItem(SESSION_STORAGE_KEY.SEARCH_INITIAL_MSG, question) + window.open('/search') + setIsSearchOpen(false) + } + return ( -
+
@@ -210,6 +226,29 @@ function MainPanel() { } target="_blank"> Code Browser + {searchFlag.value && isChatEnabled && ( + + +
+
+ +
+
+ Search +
+
+
+ +
+ logo +

+ The Private Search Assistant +

+ +
+
+
+ )} } onClick={handleSignOut}> Sign out {signOutLoading && } diff --git a/ee/tabby-ui/app/globals.css b/ee/tabby-ui/app/globals.css index a8bfbe91edde..bf209eb9a159 100644 --- a/ee/tabby-ui/app/globals.css +++ b/ee/tabby-ui/app/globals.css @@ -91,3 +91,7 @@ .prose-full-width { max-width: none !important; } + +.dialog-without-close-btn > button { + display: none; +} diff --git a/ee/tabby-ui/app/search/components/search.css b/ee/tabby-ui/app/search/components/search.css new file mode 100644 index 000000000000..8ae3228ba942 --- /dev/null +++ b/ee/tabby-ui/app/search/components/search.css @@ -0,0 +1,18 @@ +@keyframes sparkle { + 0% { + opacity: 1; + transform: scaleX(1); + } + 50% { + opacity: 0.5; + transform: scaleX(-1); + } + 100% { + opacity: 1; + transform: scaleX(1); + } +} + +.sparkle-animation { + animation: sparkle 2s infinite; +} \ No newline at end of file diff --git a/ee/tabby-ui/app/search/components/search.tsx b/ee/tabby-ui/app/search/components/search.tsx new file mode 100644 index 000000000000..8ea19624a365 --- /dev/null +++ b/ee/tabby-ui/app/search/components/search.tsx @@ -0,0 +1,746 @@ +'use client' + +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react' +import Image from 'next/image' +import logoUrl from '@/assets/tabby.png' +import { Message } from 'ai' +import { nanoid } from 'nanoid' +import remarkGfm from 'remark-gfm' +import remarkMath from 'remark-math' + +import { SESSION_STORAGE_KEY } from '@/lib/constants' +import { useEnableSearch } from '@/lib/experiment-flags' +import { useIsChatEnabled } from '@/lib/hooks/use-server-info' +import { useTabbyAnswer } from '@/lib/hooks/use-tabby-answer' +import fetcher from '@/lib/tabby/fetcher' +import { AnswerRequest } from '@/lib/types' +import { cn } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { CodeBlock } from '@/components/ui/codeblock' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger +} from '@/components/ui/hover-card' +import { + IconBlocks, + IconChevronRight, + IconLayers, + IconPlus, + IconRefresh, + IconSparkles, + IconStop +} from '@/components/ui/icons' +import { ScrollArea } from '@/components/ui/scroll-area' +import { Separator } from '@/components/ui/separator' +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTrigger +} from '@/components/ui/sheet' +import { Skeleton } from '@/components/ui/skeleton' +import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' +import { CopyButton } from '@/components/copy-button' +import { BANNER_HEIGHT, useShowDemoBanner } from '@/components/demo-banner' +import { MemoizedReactMarkdown } from '@/components/markdown' +import TextAreaSearch from '@/components/textarea-search' + +import './search.css' + +interface Source { + title: string + link: string + snippet: string +} + +type ConversationMessage = Message & { + relevant_documents?: { + title: string + link: string + snippet: string + }[] + relevant_questions?: string[] + isLoading?: boolean + error?: string +} + +type SearchContextValue = { + isLoading: boolean + onRegenerateResponse: (id: string) => void + onSubmitSearch: (question: string) => void +} + +export const SearchContext = createContext( + {} as SearchContextValue +) + +const tabbyFetcher = ((url: string, init?: RequestInit) => { + return fetcher(url, { + ...init, + responseFormatter(response) { + return response + }, + errorHandler(response) { + throw new Error(response ? String(response.status) : 'Fail to fetch') + } + }) +}) as typeof fetch + +export function Search() { + const isChatEnabled = useIsChatEnabled() + const [searchFlag] = useEnableSearch() + const [isShowDemoBanner] = useShowDemoBanner() + const [conversation, setConversation] = useState([]) + const [showStop, setShowStop] = useState(false) + const [container, setContainer] = useState(null) + const [title, setTitle] = useState('') + const [currentLoadindId, setCurrentLoadingId] = useState('') + const contentContainerRef = useRef(null) + + const { triggerRequest, isLoading, error, answer, stop } = useTabbyAnswer({ + fetcher: tabbyFetcher + }) + + useEffect(() => { + const initialQuestion = sessionStorage.getItem( + SESSION_STORAGE_KEY.SEARCH_INITIAL_MSG + ) + if (initialQuestion) { + onSubmitSearch(initialQuestion) + sessionStorage.removeItem(SESSION_STORAGE_KEY.SEARCH_INITIAL_MSG) + } + }, []) + + useEffect(() => { + if (title) document.title = title + }, [title]) + + useEffect(() => { + setContainer( + contentContainerRef?.current?.children[1] as HTMLDivElement | null + ) + }, [contentContainerRef?.current]) + + // Handling the stream response from useTabbyAnswer + useEffect(() => { + if (!answer) return + const newConversation = [...conversation] + const currentAnswer = newConversation.find( + item => item.id === currentLoadindId + ) + if (!currentAnswer) return + currentAnswer.content = answer.answer_delta || '' + currentAnswer.relevant_documents = answer.relevant_documents + currentAnswer.relevant_questions = answer.relevant_questions + currentAnswer.isLoading = isLoading + setConversation(newConversation) + }, [isLoading, answer]) + + // Handling the error response from useTabbyAnswer + useEffect(() => { + if (error) { + const newConversation = [...conversation] + const currentAnswer = newConversation.find( + item => item.id === currentLoadindId + ) + if (currentAnswer) { + currentAnswer.error = + error.message === '401' ? 'Unauthorized' : 'Fail to fetch' + currentAnswer.isLoading = false + } + } + }, [error]) + + // Delay showing the stop button + useEffect(() => { + if (isLoading && !showStop) { + setTimeout(() => { + setShowStop(true) + + // Scroll to the bottom + if (container) { + container.scrollTo({ + top: container.scrollHeight, + behavior: 'smooth' + }) + } + }, 1500) + } + + if (!isLoading && showStop) { + setShowStop(false) + } + }, [isLoading]) + + const onSubmitSearch = (question: string) => { + // FIXME: code query? extra from user's input? + const previousMessages = conversation.map(message => ({ + role: message.role, + id: message.id, + content: message.content + })) + const previousUserId = previousMessages.length > 0 && previousMessages[0].id + const newAssistantId = nanoid() + const newUserMessage: ConversationMessage = { + id: previousUserId || nanoid(), + role: 'user', + content: question + } + const newAssistantMessage: ConversationMessage = { + id: newAssistantId, + role: 'assistant', + content: '', + isLoading: true + } + + const answerRequest: AnswerRequest = { + messages: [...previousMessages, newUserMessage], + doc_query: true, + generate_relevant_questions: true + } + + setCurrentLoadingId(newAssistantId) + setConversation( + [...conversation].concat([newUserMessage, newAssistantMessage]) + ) + triggerRequest(answerRequest) + + // Update HTML page title + if (!title) setTitle(question) + } + + const onRegenerateResponse = (id: string) => { + const targetAnswerIdx = conversation.findIndex(item => item.id === id) + if (targetAnswerIdx < 1) return + const targetQuestionIdx = targetAnswerIdx - 1 + const targetQuestion = conversation[targetQuestionIdx] + + const previousMessages = conversation + .slice(0, targetQuestionIdx) + .map(message => ({ + role: message.role, + id: message.id, + content: message.content + })) + const newUserMessage = { + role: 'user', + id: targetQuestion.id, + content: targetQuestion.content + } + const answerRequest: AnswerRequest = { + messages: [...previousMessages, newUserMessage], + doc_query: true, + generate_relevant_questions: true + } + + const newConversation = [...conversation] + let newTargetAnswer = newConversation[targetAnswerIdx] + newTargetAnswer.content = '' + newTargetAnswer.error = undefined + newTargetAnswer.isLoading = true + + setCurrentLoadingId(newTargetAnswer.id) + setConversation(newConversation) + triggerRequest(answerRequest) + } + + if (!searchFlag.value || !isChatEnabled) { + return <> + } + + const noConversation = conversation.length === 0 + const style = isShowDemoBanner + ? { height: `calc(100vh - ${BANNER_HEIGHT})` } + : { height: '100vh' } + return ( + +
+ +
+
+ {conversation.map((item, idx) => { + if (item.role === 'user') { + return ( +
+ {idx !== 0 && } +
+ +
+
+ ) + } + if (item.role === 'assistant') { + return ( +
+ +
+ ) + } + return <> + })} +
+
+
+ + {container && ( + + )} + +
+ {noConversation && ( + <> + logo +

+ The Private Search Assistant +

+ + )} + {!isLoading && ( +
+ +
+ )} + +
+
+
+ ) +} + +function AnswerBlock({ + question, + answer, + showRelatedQuestion +}: { + question: string + answer: ConversationMessage + showRelatedQuestion: boolean +}) { + const { onRegenerateResponse, onSubmitSearch, isLoading } = + useContext(SearchContext) + + const getCopyContent = (answer: ConversationMessage) => { + if (!answer.relevant_documents) return answer.content + + const citationMatchRegex = /\[\[?citation:\s*\d+\]?\]/g + const content = answer.content + .replace(citationMatchRegex, (match, p1) => { + const citationNumberMatch = match?.match(/\d+/) + return `[${citationNumberMatch}]` + }) + .trim() + const citations = answer.relevant_documents + .map((relevent, idx) => `[${idx + 1}] ${relevent.link}`) + .join('\n') + return `${content}\n\nCitations:\n${citations}` + } + + return ( +
+ {/* Relevant documents */} + {answer.relevant_documents && answer.relevant_documents.length > 0 && ( +
+
+ +

Source

+
+
+ {answer.relevant_documents.slice(0, 3).map((source, index) => ( + + ))} + {answer.relevant_documents && + answer.relevant_documents.length > 3 && ( + + +
+
+

Check more

+ +
+
+ {answer.relevant_documents + .slice(3, 6) + .map((source, idx) => { + const { hostname } = new URL(source.link) + return ( + + ) + })} +
+
+
+ + + + {answer.relevant_documents.length} resources + + + +
+ {answer.relevant_documents.map((source, index) => ( + + ))} +
+
+
+
+ )} +
+
+ )} + + {/* Answer content */} +
+
+ +

Answer

+
+ {answer.isLoading && !answer.content && ( + + )} + + + {answer.error && } + + {!answer.isLoading && ( +
+ + {!isLoading && ( + + )} +
+ )} +
+ + {/* Related questions */} + {showRelatedQuestion && + !answer.isLoading && + answer.relevant_questions && + answer.relevant_questions.length > 0 && ( +
+
+ +

Related

+
+
+ {answer.relevant_questions?.map((related, index) => ( +
+

+ {related} +

+ +
+ ))} +
+
+ )} +
+ ) +} + +function SourceBlock({ source, index }: { source: Source; index: number }) { + return ( +
+

{index}.

+
window.open(source.link)} + > +

{source.title}

+

{source.snippet}

+
+
+ ) +} + +function SourceCard({ source, index }: { source: Source; index: number }) { + const { hostname } = new URL(source.link) + return ( +
window.open(source.link)} + > +

+ {source.title} +

+
+ + +
+

+ {hostname.replace('www.', '').split('.')[0]} +

+ . +

{index}

+
+
+
+ ) +} + +function MessageMarkdown({ + message, + headline = false, + sources +}: { + message: string + headline?: boolean + sources?: Source[] +}) { + return ( + + {children} + + ) + } + + if (children.length) { + return ( +
+ {children.map((childrenItem, index) => { + if (typeof childrenItem === 'string') { + const citationMatchRegex = /\[\[?citation:\s*\d+\]?\]/g + const textList = childrenItem.split(citationMatchRegex) + const citationList = childrenItem.match(citationMatchRegex) + return ( + + {textList.map((text, index) => { + const citation = citationList?.[index] + const citationNumberMatch = citation?.match(/\d+/) + const citationIndex = citationNumberMatch + ? parseInt(citationNumberMatch[0], 10) + : null + const source = + citationIndex !== null + ? sources?.[citationIndex - 1] + : null + const sourceUrl = source ? new URL(source.link) : null + return ( + + {text && {text}} + {source && ( + + + window.open(source.link)} + > + {citationIndex} + + + +
+
+ +

+ {sourceUrl!.hostname} +

+
+

window.open(source.link)} + > + {source.title} +

+

+ {source.snippet} +

+
+
+
+ )} +
+ ) + })} +
+ ) + } + + return {childrenItem} + })} +
+ ) + } + + return

{children}

+ }, + code({ node, inline, className, children, ...props }) { + if (children.length) { + if (children[0] == '▍') { + return ( + + ) + } + + children[0] = (children[0] as string).replace('`▍`', '▍') + } + + const match = /language-(\w+)/.exec(className || '') + + if (inline) { + return ( + + {children} + + ) + } + + return ( + + ) + } + }} + > + {message} +
+ ) +} + +function SiteFavicon({ + hostname, + className +}: { + hostname: string + className?: string +}) { + return ( + {hostname} + ) +} + +function ErrorMessageBlock({ error = 'Fail to fetch' }: { error?: string }) { + const errorMessage = useMemo(() => { + let jsonString = JSON.stringify( + { + error: true, + message: error + }, + null, + 2 + ) + const markdownJson = '```\n' + jsonString + '\n```' + return markdownJson + }, [error]) + return ( + + {children} +
+ ) + } + }} + > + {errorMessage} + + ) +} diff --git a/ee/tabby-ui/app/search/page.tsx b/ee/tabby-ui/app/search/page.tsx new file mode 100644 index 000000000000..508b4f730fef --- /dev/null +++ b/ee/tabby-ui/app/search/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import { Search } from './components/search' + +export const metadata: Metadata = { + title: 'Search' +} + +export default function SearchPage() { + return +} diff --git a/ee/tabby-ui/components/chat/chat.tsx b/ee/tabby-ui/components/chat/chat.tsx index 68ed4cc9e912..014c08b52a30 100644 --- a/ee/tabby-ui/components/chat/chat.tsx +++ b/ee/tabby-ui/components/chat/chat.tsx @@ -16,12 +16,12 @@ import { } from '@/lib/types/chat' import { cn, nanoid } from '@/lib/utils' +import { useTabbyAnswer } from '../../lib/hooks/use-tabby-answer' import { ListSkeleton } from '../skeleton' import { ChatPanel } from './chat-panel' import { ChatScrollAnchor } from './chat-scroll-anchor' import { EmptyScreen } from './empty-screen' import { QuestionAnswerList } from './question-answer' -import { useTabbyAnswer } from './use-tabby-answer' type ChatContextValue = { isLoading: boolean diff --git a/ee/tabby-ui/components/copy-button.tsx b/ee/tabby-ui/components/copy-button.tsx index dffaaeae2a60..4f147fc103a4 100644 --- a/ee/tabby-ui/components/copy-button.tsx +++ b/ee/tabby-ui/components/copy-button.tsx @@ -10,12 +10,14 @@ import { IconCheck, IconCopy } from './ui/icons' interface CopyButtonProps extends ButtonProps { value: string onCopyContent?: (value: string) => void + text?: string } export function CopyButton({ className, value, onCopyContent, + text, ...props }: CopyButtonProps) { const { isCopied, copyToClipboard } = useCopyToClipboard({ @@ -33,13 +35,14 @@ export function CopyButton({ return ( ) } diff --git a/ee/tabby-ui/components/textarea-search.tsx b/ee/tabby-ui/components/textarea-search.tsx new file mode 100644 index 000000000000..14f714027b5e --- /dev/null +++ b/ee/tabby-ui/components/textarea-search.tsx @@ -0,0 +1,83 @@ +'use client' + +import { useEffect, useState } from 'react' +import TextareaAutosize from 'react-textarea-autosize' + +import { cn } from '@/lib/utils' + +import { IconArrowRight } from './ui/icons' + +export default function TextAreaSearch({ + onSearch, + className, + placeholder +}: { + onSearch: (value: string) => void + className?: string + placeholder?: string +}) { + const [isShow, setIsShow] = useState(false) + const [isFocus, setIsFocus] = useState(false) + const [value, setValue] = useState('') + + useEffect(() => { + // Ensure the textarea height remains consistent during rendering + setIsShow(true) + }, []) + + const onSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) return e.preventDefault() + } + + const onSearchKeyUp = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + return search() + } + } + + const search = () => { + if (!value) return + onSearch(value) + setValue('') + } + + return ( +
+ setIsFocus(true)} + onBlur={() => setIsFocus(false)} + onChange={e => setValue(e.target.value)} + value={value} + /> +
0 + } + )} + onClick={search} + > + +
+
+ ) +} diff --git a/ee/tabby-ui/components/ui/dialog.tsx b/ee/tabby-ui/components/ui/dialog.tsx index 849798504b3b..5509e2ddbfca 100644 --- a/ee/tabby-ui/components/ui/dialog.tsx +++ b/ee/tabby-ui/components/ui/dialog.tsx @@ -2,26 +2,17 @@ import * as React from 'react' import * as DialogPrimitive from '@radix-ui/react-dialog' +import { X } from 'lucide-react' import { cn } from '@/lib/utils' -import { IconClose } from '@/components/ui/icons' const Dialog = DialogPrimitive.Root const DialogTrigger = DialogPrimitive.Trigger -const DialogPortal = ({ - className, - children, - ...props -}: DialogPrimitive.DialogPortalProps) => ( - -
- {children} -
-
-) -DialogPortal.displayName = DialogPrimitive.Portal.displayName +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close const DialogOverlay = React.forwardRef< React.ElementRef, @@ -30,7 +21,7 @@ const DialogOverlay = React.forwardRef< {children} - + Close @@ -119,6 +110,9 @@ DialogDescription.displayName = DialogPrimitive.Description.displayName export { Dialog, + DialogPortal, + DialogOverlay, + DialogClose, DialogTrigger, DialogContent, DialogHeader, diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx index fe43d205bfe0..96195dd7fd65 100644 --- a/ee/tabby-ui/components/ui/icons.tsx +++ b/ee/tabby-ui/components/ui/icons.tsx @@ -2,7 +2,17 @@ import * as React from 'react' // FIXME(wwayne): Review each icons and consider re-export from `lucide-react`. -import { BookOpenText, ChevronsDownUp, GitFork, Mail, Star } from 'lucide-react' +import { + Blocks, + BookOpenText, + ChevronsDownUp, + GitFork, + Layers2, + Mail, + Search, + Sparkles, + Star +} from 'lucide-react' import { cn } from '@/lib/utils' @@ -1414,6 +1424,13 @@ const IconGitFork = ({ ) +function IconBlocks({ + className, + ...props +}: React.ComponentProps) { + return +} + function IconVSCode({ className, ...props }: React.ComponentProps<'svg'>) { return ( ) { ) } +function IconLayers({ + className, + ...props +}: React.ComponentProps) { + return +} + +function IconSparkles({ + className, + ...props +}: React.ComponentProps) { + return +} + +function IconSearch({ + className, + ...props +}: React.ComponentProps) { + return +} + export { IconEdit, IconNextChat, @@ -1524,6 +1562,10 @@ export { IconChevronsDownUp, IconStar, IconGitFork, + IconBlocks, IconVSCode, - IconJetBrains + IconJetBrains, + IconLayers, + IconSparkles, + IconSearch } diff --git a/ee/tabby-ui/components/ui/sheet.tsx b/ee/tabby-ui/components/ui/sheet.tsx index 442020bf64b8..94dd7c88497a 100644 --- a/ee/tabby-ui/components/ui/sheet.tsx +++ b/ee/tabby-ui/components/ui/sheet.tsx @@ -2,9 +2,10 @@ import * as React from 'react' import * as SheetPrimitive from '@radix-ui/react-dialog' +import { cva, type VariantProps } from 'class-variance-authority' +import { X } from 'lucide-react' import { cn } from '@/lib/utils' -import { IconClose } from '@/components/ui/icons' const Sheet = SheetPrimitive.Root @@ -12,27 +13,15 @@ const SheetTrigger = SheetPrimitive.Trigger const SheetClose = SheetPrimitive.Close -const SheetPortal = ({ - className, - children, - ...props -}: SheetPrimitive.DialogPortalProps) => ( - - {children} - -) -SheetPortal.displayName = SheetPrimitive.Portal.displayName +const SheetPortal = SheetPrimitive.Portal const SheetOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( +>(({ className, ...props }, ref) => ( , + VariantProps {} + const SheetContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + SheetContentProps +>(({ side = 'right', className, children, ...props }, ref) => ( + {children} - + Close @@ -68,7 +78,13 @@ const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( -
+
) SheetHeader.displayName = 'SheetHeader' @@ -112,6 +128,8 @@ SheetDescription.displayName = SheetPrimitive.Description.displayName export { Sheet, + SheetPortal, + SheetOverlay, SheetTrigger, SheetClose, SheetContent, diff --git a/ee/tabby-ui/lib/constants/index.ts b/ee/tabby-ui/lib/constants/index.ts index 7806dd822185..e74797c71689 100644 --- a/ee/tabby-ui/lib/constants/index.ts +++ b/ee/tabby-ui/lib/constants/index.ts @@ -3,5 +3,6 @@ export const PLACEHOLDER_EMAIL_FORM = 'name@yourcompany.com' export const DEFAULT_PAGE_SIZE = 20 export const SESSION_STORAGE_KEY = { - DEMO_AUTO_LOGIN: '_tabby_demo_autologin' + DEMO_AUTO_LOGIN: '_tabby_demo_autologin', + SEARCH_INITIAL_MSG: '_tabby_search_initial_msg' } diff --git a/ee/tabby-ui/lib/experiment-flags.ts b/ee/tabby-ui/lib/experiment-flags.ts index 009c1e57e900..332048ab4b4b 100644 --- a/ee/tabby-ui/lib/experiment-flags.ts +++ b/ee/tabby-ui/lib/experiment-flags.ts @@ -101,8 +101,16 @@ const enableCodeBrowserQuickActionBarFactory = new ExperimentFlagFactory( 'Enable Quick Action Bar to display a convenient toolbar when you select code, offering options to explain the code, add unit tests, and more.', true ) - export const EXP_enable_code_browser_quick_action_bar = enableCodeBrowserQuickActionBarFactory.defineGlobalVar() export const useEnableCodeBrowserQuickActionBar = enableCodeBrowserQuickActionBarFactory.defineHook() + +const enableSearchFactory = new ExperimentFlagFactory( + 'enable_search', + 'Search', + 'Enable the search on the home page to search for anything you want to know using the local chat model.', + false +) +export const EXP_enable_search = enableSearchFactory.defineGlobalVar() +export const useEnableSearch = enableSearchFactory.defineHook() diff --git a/ee/tabby-ui/components/chat/use-tabby-answer.ts b/ee/tabby-ui/lib/hooks/use-tabby-answer.ts similarity index 94% rename from ee/tabby-ui/components/chat/use-tabby-answer.ts rename to ee/tabby-ui/lib/hooks/use-tabby-answer.ts index 75296736e5df..90825e87117c 100644 --- a/ee/tabby-ui/components/chat/use-tabby-answer.ts +++ b/ee/tabby-ui/lib/hooks/use-tabby-answer.ts @@ -52,7 +52,11 @@ export function useTabbyAnswer({ // merge answer_delta answer_delta: `${existingData?.answer_delta ?? ''}${ data?.answer_delta ?? '' - }` + }`, + relevant_documents: + data?.relevant_documents || existingData.relevant_documents, + relevant_questions: + data?.relevant_questions || existingData.relevant_questions } } diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json index cfb56cff25be..b2e7c96eb7f0 100644 --- a/ee/tabby-ui/package.json +++ b/ee/tabby-ui/package.json @@ -31,7 +31,7 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", - "@radix-ui/react-dialog": "1.0.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", @@ -89,7 +89,7 @@ "react-nice-avatar": "^1.5.0", "react-resizable-panels": "^1.0.7", "react-syntax-highlighter": "^15.5.0", - "react-textarea-autosize": "^8.4.1", + "react-textarea-autosize": "^8.5.3", "react-topbar-progress-indicator": "^4.1.1", "recharts": "^2.12.4", "rehype-raw": "6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51d69a905fb4..3b4ccfb56ba1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -428,8 +428,8 @@ importers: specifier: ^1.0.3 version: 1.0.3(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-dialog': - specifier: 1.0.4 - version: 1.0.4(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + specifier: ^1.0.5 + version: 1.0.5(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-dropdown-menu': specifier: ^2.0.5 version: 2.0.6(@types/react-dom@18.2.8)(@types/react@18.2.23)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -602,7 +602,7 @@ importers: specifier: ^15.5.0 version: 15.5.0(react@18.2.0) react-textarea-autosize: - specifier: ^8.4.1 + specifier: ^8.5.3 version: 8.5.3(@types/react@18.2.23)(react@18.2.0) react-topbar-progress-indicator: specifier: ^4.1.1