Skip to content

Commit

Permalink
feat(chat): allow share a thread from Chat panel sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
liangfung committed Nov 27, 2024
1 parent e5f3e06 commit ffa5951
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 21 deletions.
3 changes: 3 additions & 0 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export interface ClientApi {
onCopy: (content: string) => void

onKeyboardEvent: (type: 'keydown' | 'keyup' | 'keypress', event: KeyboardEventInit) => void

onOpenExternal: (url: string) => Promise<boolean>
}

export interface ChatMessage {
Expand All @@ -94,6 +96,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApi): ServerA
onLoaded: api.onLoaded,
onCopy: api.onCopy,
onKeyboardEvent: api.onKeyboardEvent,
onOpenExternal: api.onOpenExternal,
},
})
}
Expand Down
5 changes: 5 additions & 0 deletions clients/vscode/src/chat/WebviewHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,11 @@ export class WebviewHelper {
this.logger.debug(`Dispatching keyboard event: ${type} ${JSON.stringify(event)}`);
this.webview?.postMessage({ action: "dispatchKeyboardEvent", type, event });
},
onOpenExternal: async (url: string) => {
this.logger.info('Open external', url)
const success = await env.openExternal(Uri.parse(url));
return success;
}
});
}
}
1 change: 1 addition & 0 deletions clients/vscode/src/chat/chatPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function createClient(webview: Webview, api: ClientApi): ServerApi {
onCopy: api.onCopy,
onLoaded: api.onLoaded,
onKeyboardEvent: api.onKeyboardEvent,
onOpenExternal: api.onOpenExternal
},
});
}
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 @@ impl Mutation {

/// 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
20 changes: 19 additions & 1 deletion ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import Image from 'next/image'
import { useSearchParams } from 'next/navigation'
import tabbyUrl from '@/assets/tabby.png'
Expand Down Expand Up @@ -263,6 +263,23 @@ export default function ChatPage() {
server?.navigate(context, opts)
}

const onOpenExternal = useMemo(() => {
// FIXME check capability
const hasCapability = true
if (isInEditor && !hasCapability) {
return undefined
}

return async (url: string) => {
if (isInEditor) {
return server?.onOpenExternal(url)
} else {
const success = window.open(url, '_blank')
return !!success
}
}
}, [isInEditor, server])

const refresh = async () => {
setIsRefreshLoading(true)
await server?.refresh()
Expand Down Expand Up @@ -370,6 +387,7 @@ export default function ChatPage() {
onCopyContent={isInEditor && server?.onCopy}
onSubmitMessage={isInEditor && server?.onSubmitMessage}
onApplyInEditor={isInEditor && server?.onApplyInEditor}
onOpenExternal={onOpenExternal}
/>
</ErrorBoundary>
)
Expand Down
9 changes: 2 additions & 7 deletions ee/tabby-ui/app/search/components/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ import { useMutation } from '@/lib/tabby/gql'
import {
contextInfoQuery,
listThreadMessages,
listThreads
listThreads,
setThreadPersistedMutation
} from '@/lib/tabby/query'
import {
AttachmentCodeItem,
Expand Down Expand Up @@ -924,12 +925,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
81 changes: 70 additions & 11 deletions ee/tabby-ui/components/chat/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import React, { RefObject } from 'react'
import React, { RefObject, useMemo } from 'react'
import slugify from '@sindresorhus/slugify'
import type { UseChatHelpers } from 'ai/react'
import { AnimatePresence, motion } from 'framer-motion'
import { compact } from 'lodash-es'
import { toast } from 'sonner'
import type { Context } from 'tabby-chat-panel'

import { SLUG_TITLE_MAX_LENGTH } from '@/lib/constants'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { updateEnableActiveSelection } from '@/lib/stores/chat-actions'
import { useChatStore } from '@/lib/stores/chat-store'
import { cn } from '@/lib/utils'
import { useMutation } from '@/lib/tabby/gql'
import { setThreadPersistedMutation } from '@/lib/tabby/query'
import { cn, getTitleFromMessages } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
IconEye,
IconEyeOff,
IconRefresh,
IconRemove,
IconShare,
IconStop,
IconTrash
} from '@/components/ui/icons'
Expand Down Expand Up @@ -51,18 +59,57 @@ function ChatPanelRenderer(
) {
const promptFormRef = React.useRef<PromptFormRef>(null)
const {
threadId,
container,
onClearMessages,
qaPairs,
isLoading,
relevantContext,
removeRelevantContext,
activeSelection
activeSelection,
onOpenExternal
} = React.useContext(ChatContext)
const enableActiveSelection = useChatStore(
state => state.enableActiveSelection
)

const slugWithThreadId = useMemo(() => {
if (!threadId) return ''
const content = qaPairs[0]?.user.message
if (!content) return threadId

const title = getTitleFromMessages([], content, {
maxLength: SLUG_TITLE_MAX_LENGTH
})
const slug = slugify(title)
const slugWithThreadId = compact([slug, threadId]).join('-')
return slugWithThreadId
}, [qaPairs[0]?.user.message, threadId])

const { isCopied, copyToClipboard } = useCopyToClipboard({
timeout: 2000
})

const setThreadPersisted = useMutation(setThreadPersistedMutation, {
onError(err) {
toast.error(err.message)
}
})

const handleShareThread = async () => {
if (!threadId) return

const result = await setThreadPersisted({ threadId })
if (!result?.data?.setThreadPersisted) return

let url = new URL(window.location.origin)
url.pathname = `/search/${slugWithThreadId}`

if (onOpenExternal) {
await onOpenExternal(url.toString())
}
}

React.useImperativeHandle(
ref,
() => {
Expand Down Expand Up @@ -91,14 +138,26 @@ function ChatPanelRenderer(
</Button>
) : (
qaPairs?.length > 0 && (
<Button
variant="outline"
onClick={() => reload()}
className="bg-background"
>
<IconRefresh className="mr-2" />
Regenerate response
</Button>
<>
<Button
variant="outline"
onClick={() => reload()}
className="bg-background"
>
<IconRefresh className="mr-2" />
Regenerate
</Button>
{!!onOpenExternal && (
<Button
variant="outline"
className="bg-background"
onClick={handleShareThread}
>
<IconShare className="mr-2" />
Share
</Button>
)}
</>
)
)}
{qaPairs?.length > 0 && (
Expand Down
8 changes: 7 additions & 1 deletion ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { EmptyScreen } from './empty-screen'
import { QuestionAnswerList } from './question-answer'

type ChatContextValue = {
threadId: string | undefined
isLoading: boolean
qaPairs: QuestionAnswerPair[]
handleMessageAction: (
Expand All @@ -50,6 +51,7 @@ type ChatContextValue = {
activeSelection: Context | null
removeRelevantContext: (index: number) => void
chatInputRef: RefObject<HTMLTextAreaElement>
onOpenExternal?: (url: string) => Promise<boolean | undefined>
}

export const ChatContext = React.createContext<ChatContextValue>(
Expand Down Expand Up @@ -85,6 +87,7 @@ interface ChatProps extends React.ComponentProps<'div'> {
opts?: { languageId: string; smart: boolean }
) => void
chatInputRef: RefObject<HTMLTextAreaElement>
onOpenExternal?: (url: string) => Promise<boolean | undefined>
}

function ChatRenderer(
Expand All @@ -104,6 +107,7 @@ function ChatRenderer(
onCopyContent,
onSubmitMessage,
onApplyInEditor,
onOpenExternal,
chatInputRef
}: ChatProps,
ref: React.ForwardedRef<ChatRef>
Expand Down Expand Up @@ -520,6 +524,7 @@ function ChatRenderer(
return (
<ChatContext.Provider
value={{
threadId,
isLoading,
qaPairs,
onNavigateToContext,
Expand All @@ -531,7 +536,8 @@ function ChatRenderer(
relevantContext,
removeRelevantContext,
chatInputRef,
activeSelection
activeSelection,
onOpenExternal
}}
>
<div className="flex justify-center overflow-x-hidden">
Expand Down
6 changes: 6 additions & 0 deletions ee/tabby-ui/lib/tabby/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,3 +445,9 @@ export const listThreadMessages = graphql(/* GraphQL */ `
}
}
`)

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

0 comments on commit ffa5951

Please sign in to comment.