From f89f3e51e5cdcc19ecf33563b9bcef9116b43b31 Mon Sep 17 00:00:00 2001 From: wwayne Date: Wed, 5 Jun 2024 16:50:21 +0800 Subject: [PATCH] copy and regenerate --- ee/tabby-ui/app/search/components/search.tsx | 243 ++++++++++++------- ee/tabby-ui/components/copy-button.tsx | 7 +- 2 files changed, 157 insertions(+), 93 deletions(-) diff --git a/ee/tabby-ui/app/search/components/search.tsx b/ee/tabby-ui/app/search/components/search.tsx index da53e2076e4e..dc42917995b8 100644 --- a/ee/tabby-ui/app/search/components/search.tsx +++ b/ee/tabby-ui/app/search/components/search.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { createContext, useContext, useEffect, useRef, useState } from 'react' import Image from 'next/image' import logoUrl from '@/assets/tabby.png' import { Message } from 'ai' @@ -17,7 +17,6 @@ import { CodeBlock } from '@/components/ui/codeblock' import { IconArrowRight, IconBlocks, - IconCopy, IconLayers, IconPlus, IconRefresh, @@ -38,6 +37,7 @@ import { Skeleton } from '@/components/ui/skeleton' import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' // FIXME: move to lib/hooks import { useTabbyAnswer } from '@/components/chat/use-tabby-answer' +import { CopyButton } from '@/components/copy-button' import { MemoizedReactMarkdown } from '@/components/markdown' import './search.css' @@ -58,6 +58,15 @@ type ConversationMessage = Message & { isLoading?: boolean } +type SearchContextValue = { + isLoading: boolean + onRegenerateResponse: (id: string) => void +} + +export const SearchContext = createContext( + {} as SearchContextValue +) + const tabbyFetcher = ((url: string, init?: RequestInit) => { return fetcher(url, { ...init, @@ -85,8 +94,9 @@ export default function Search() { const contentContainerRef = useRef(null) const [container, setContainer] = useState(null) const [title, setTitle] = useState('') + const [currentLoadindId, setCurrentLoadingId] = useState('') - // FIXME: error and stop + // FIXME: error const { triggerRequest, isLoading, error, answer, stop } = useTabbyAnswer({ fetcher: tabbyFetcher }) @@ -106,7 +116,10 @@ export default function Search() { useEffect(() => { if (!answer) return const newConversation = [...conversation] - let currentAnswer = newConversation[newConversation.length - 1] + let 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 @@ -122,13 +135,14 @@ export default function Search() { 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: nanoid(), + id: newAssistantId, role: 'assistant', content: '', isLoading: true @@ -140,6 +154,7 @@ export default function Search() { generate_relevant_questions: true } + setCurrentLoadingId(newAssistantId) setConversation( [...conversation].concat([newUserMessage, newAssistantMessage]) ) @@ -159,96 +174,137 @@ export default function Search() { 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.isLoading = true + + setCurrentLoadingId(newTargetAnswer.id) + setConversation(newConversation) + triggerRequest(answerRequest) + } + const noConversation = conversation.length === 0 const currentAnswerHasContent = Boolean( conversation[conversation.length - 1]?.content ) // FIXME: the height considering demo banner return ( -
- -
-
- {conversation.map((item, idx) => { - if (item.role === 'user') { - return ( -
- {idx !== 0 && } -
- + +
+ +
+
+ {conversation.map((item, idx) => { + if (item.role === 'user') { + return ( +
+ {idx !== 0 && } +
+ +
-
- ) - } - if (item.role === 'assistant') { - return ( -
- -
- ) - } - return <> - })} -
-
- - - {/* FIXME: adjust position in small width */} - {container && ( - - )} - -
- {noConversation && ( - <> - logo -

- Your private search engine (TODO) -

- - )} - {!isLoading && ( -
- + ) + } + if (item.role === 'assistant') { + return ( +
+ +
+ ) + } + return <> + })} +
+ + + {/* FIXME: adjust position in small width */} + {container && ( + )} - + {noConversation && ( + <> + logo +

+ Your private search engine (TODO) +

+ + )} + {!isLoading && ( +
+ +
+ )} + +
-
+ ) } @@ -324,6 +380,7 @@ function AnswerBlock({ question: string answer: ConversationMessage }) { + const { onRegenerateResponse, isLoading } = useContext(SearchContext) return (
{/* Relevant documents */} @@ -407,14 +464,21 @@ function AnswerBlock({ {!answer.isLoading && (
-
- -

Copy

-
-
- -

Regenerate

-
+ + {!isLoading && ( + + )}
)}
@@ -491,8 +555,6 @@ function MessageMarkdown({ message: string headline?: boolean }) { - // FIXME: onCopyContent - // const { onCopyContent } = React.useContext(ChatContext) return ( ) 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 ( ) }