Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chat): allow share a thread from Chat panel sidebar #3479

Merged
merged 10 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ee/tabby-schema/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,7 +1178,7 @@

/// Turn on persisted status for a thread.
async fn set_thread_persisted(ctx: &Context, thread_id: ID) -> Result<bool> {
let user = check_user(ctx).await?;
let user = check_user_allow_auth_token(ctx).await?;

Check warning on line 1181 in ee/tabby-schema/src/schema/mod.rs

View check run for this annotation

Codecov / codecov/patch

ee/tabby-schema/src/schema/mod.rs#L1181

Added line #L1181 was not covered by tests
let svc = ctx.locator.thread();
let Some(thread) = svc.get(&thread_id).await? else {
return Err(CoreError::NotFound("Thread not found"));
Expand Down
76 changes: 59 additions & 17 deletions ee/tabby-ui/app/search/components/assistant-message-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { Context } from 'tabby-chat-panel/index'
import * as z from 'zod'

import { MARKDOWN_CITATION_REGEX } from '@/lib/constants/regex'
import { MessageAttachmentCode } from '@/lib/gql/generates/graphql'
import {
Maybe,
MessageAttachmentClientCode,
MessageAttachmentCode
} from '@/lib/gql/generates/graphql'
import { makeFormErrorHandler } from '@/lib/tabby/gql'
import {
AttachmentDocItem,
Expand Down Expand Up @@ -68,19 +72,21 @@ import {
import { ConversationMessage, SearchContext, SOURCE_CARD_STYLE } from './search'

export function AssistantMessageSection({
className,
message,
showRelatedQuestion,
isLoading,
isLastAssistantMessage,
isDeletable,
className
clientCode
}: {
className?: string
message: ConversationMessage
showRelatedQuestion: boolean
isLoading?: boolean
isLastAssistantMessage?: boolean
isDeletable?: boolean
className?: string
clientCode?: Maybe<Array<MessageAttachmentClientCode>>
}) {
const {
onRegenerateResponse,
Expand Down Expand Up @@ -133,16 +139,29 @@ export function AssistantMessageSection({

const IconAnswer = isLoading ? IconSpinner : IconSparkles

const messageAttachmentDocs = message?.attachment?.doc
const messageAttachmentCode = message?.attachment?.code
const relevantCodeGitURL = message?.attachment?.code?.[0]?.gitUrl || ''

const totalHeightInRem = messageAttachmentDocs?.length
? Math.ceil(messageAttachmentDocs.length / 4) * SOURCE_CARD_STYLE.expand +
0.5 * Math.floor(messageAttachmentDocs.length / 4) +
0.5
: 0
const clientCodeContexts: RelevantCodeContext[] = useMemo(() => {
if (!clientCode?.length) return []
return (
clientCode.map(code => {
const { startLine, endLine } = getRangeFromAttachmentCode(code)

return {
kind: 'file',
range: {
start: startLine,
end: endLine
},
filepath: code.filepath || '',
content: code.content,
git_url: relevantCodeGitURL
}
}) ?? []
)
}, [clientCode, relevantCodeGitURL])

const relevantCodeContexts: RelevantCodeContext[] = useMemo(() => {
const serverCodeContexts: RelevantCodeContext[] = useMemo(() => {
return (
message?.attachment?.code?.map(code => {
const { startLine, endLine } = getRangeFromAttachmentCode(code)
Expand All @@ -162,7 +181,25 @@ export function AssistantMessageSection({
}
}) ?? []
)
}, [message?.attachment?.code])
}, [clientCode, message?.attachment?.code])

const messageAttachmentClientCode = useMemo(() => {
return clientCode?.map(o => ({
...o,
gitUrl: relevantCodeGitURL
}))
}, [clientCode, relevantCodeGitURL])

const messageAttachmentDocs = message?.attachment?.doc
const messageAttachmentCodeLen =
(messageAttachmentClientCode?.length || 0) +
(message.attachment?.code?.length || 0)

const totalHeightInRem = messageAttachmentDocs?.length
? Math.ceil(messageAttachmentDocs.length / 4) * SOURCE_CARD_STYLE.expand +
0.5 * Math.floor(messageAttachmentDocs.length / 4) +
0.5
: 0

const onCodeContextClick = (ctx: Context) => {
if (!ctx.filepath) return
Expand Down Expand Up @@ -215,7 +252,9 @@ export function AssistantMessageSection({
}

const onCodeCitationClick = (code: MessageAttachmentCode) => {
openCodeBrowserTab(code)
if (code.gitUrl) {
openCodeBrowserTab(code)
}
}

const handleUpdateAssistantMessage = async (message: ConversationMessage) => {
Expand Down Expand Up @@ -294,14 +333,16 @@ export function AssistantMessageSection({
)}
</div>

{/* code search hits */}
{messageAttachmentCode && messageAttachmentCode.length > 0 && (
{/* attachment clientCode & code */}
{messageAttachmentCodeLen > 0 && (
<CodeReferences
contexts={relevantCodeContexts}
clientContexts={clientCodeContexts}
contexts={serverCodeContexts}
className="mt-1 text-sm"
onContextClick={onCodeContextClick}
enableTooltip={enableDeveloperMode}
showExternalLink={false}
showClientCodeIcon
onTooltipClick={() => {
setConversationIdForDev(message.id)
setDevPanelOpen(true)
Expand All @@ -324,7 +365,8 @@ export function AssistantMessageSection({
<MessageMarkdown
message={message.content}
attachmentDocs={messageAttachmentDocs}
attachmentCode={messageAttachmentCode}
attachmentClientCode={messageAttachmentClientCode}
attachmentCode={message.attachment?.code}
onCodeCitationClick={onCodeCitationClick}
onCodeCitationMouseEnter={onCodeCitationMouseEnter}
onCodeCitationMouseLeave={onCodeCitationMouseLeave}
Expand Down
84 changes: 54 additions & 30 deletions ee/tabby-ui/app/search/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
createContext,
CSSProperties,
Fragment,
useEffect,
useMemo,
useRef,
Expand Down Expand Up @@ -31,6 +32,7 @@ import {
InputMaybe,
Maybe,
Message,
MessageAttachmentClientCode,
Role
} from '@/lib/gql/generates/graphql'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
Expand All @@ -48,7 +50,8 @@ import { useMutation } from '@/lib/tabby/gql'
import {
contextInfoQuery,
listThreadMessages,
listThreads
listThreads,
setThreadPersistedMutation
} from '@/lib/tabby/query'
import {
AttachmentCodeItem,
Expand Down Expand Up @@ -102,11 +105,17 @@ export type ConversationMessage = Omit<
threadRelevantQuestions?: Maybe<string[]>
error?: string
attachment?: {
clientCode?: Maybe<Array<MessageAttachmentClientCode>> | undefined
code: Maybe<Array<AttachmentCodeItem>> | undefined
doc: Maybe<Array<AttachmentDocItem>> | undefined
}
}

type ConversationPair = {
question: ConversationMessage | null
answer: ConversationMessage | null
}

type SearchContextValue = {
// flag for initialize the pathname
isPathnameInitialized: boolean
Expand Down Expand Up @@ -452,6 +461,7 @@ export function Search() {
// get and format scores from streaming answer
if (!currentAssistantMessage.attachment?.code && !!answer.attachmentsCode) {
currentAssistantMessage.attachment = {
clientCode: null,
doc: currentAssistantMessage.attachment?.doc || null,
code:
answer.attachmentsCode.map(hit => ({
Expand All @@ -466,6 +476,7 @@ export function Search() {
// get and format scores from streaming answer
if (!currentAssistantMessage.attachment?.doc && !!answer.attachmentsDoc) {
currentAssistantMessage.attachment = {
clientCode: null,
doc:
answer.attachmentsDoc.map(hit => ({
...hit.doc,
Expand Down Expand Up @@ -618,7 +629,8 @@ export function Search() {
content: '',
attachment: {
code: null,
doc: null
doc: null,
clientCode: null
},
error: undefined
}
Expand Down Expand Up @@ -730,6 +742,25 @@ export function Search() {
200
)

const qaPairs = useMemo(() => {
const pairs: Array<ConversationPair> = []
let currentPair: ConversationPair = { question: null, answer: null }
messages.forEach(message => {
if (message.role === Role.User) {
currentPair.question = message
} else if (message.role === Role.Assistant) {
if (!currentPair.answer) {
// Take the first answer
currentPair.answer = message
pairs.push(currentPair)
currentPair = { question: null, answer: null }
}
}
})

return pairs
}, [messages])

const style = isShowDemoBanner
? { height: `calc(100vh - ${BANNER_HEIGHT})` }
: { height: '100vh' }
Expand Down Expand Up @@ -788,35 +819,34 @@ export function Search() {
<ScrollArea className="h-full" ref={contentContainerRef}>
<div className="mx-auto px-4 pb-32 lg:max-w-4xl lg:px-0">
<div className="flex flex-col">
{/* messages */}
{messages.map((message, index) => {
const isLastMessage = index === messages.length - 1
if (message.role === Role.User) {
return (
<UserMessageSection
className="pb-2 pt-8"
key={message.id}
message={message}
/>
)
} else if (message.role === Role.Assistant) {
return (
<>
{qaPairs.map((pair, index) => {
const isLastMessage = index === qaPairs.length - 1
if (!pair.question) return null

return (
<Fragment key={pair.question.id}>
{!!pair.question && (
<UserMessageSection
className="pb-2 pt-8"
key={pair.question.id}
message={pair.question}
/>
)}
{!!pair.answer && (
<AssistantMessageSection
key={message.id}
key={pair.answer.id}
className="pb-8 pt-2"
message={message}
message={pair.answer}
clientCode={pair.question?.attachment?.clientCode}
isLoading={isLoading && isLastMessage}
isLastAssistantMessage={isLastMessage}
showRelatedQuestion={isLastMessage}
isDeletable={!isLoading && messages.length > 2}
/>
{!isLastMessage && <Separator />}
</>
)
} else {
return null
}
)}
{!isLastMessage && <Separator />}
</Fragment>
)
})}
</div>
</div>
Expand Down Expand Up @@ -949,12 +979,6 @@ export function Search() {
)
}

const setThreadPersistedMutation = graphql(/* GraphQL */ `
mutation SetThreadPersisted($threadId: ID!) {
setThreadPersisted(threadId: $threadId)
}
`)

const updateThreadMessageMutation = graphql(/* GraphQL */ `
mutation UpdateThreadMessage($input: UpdateMessageInput!) {
updateThreadMessage(input: $input)
Expand Down
Loading
Loading