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 3 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: 2 additions & 0 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface ClientApiMethods {

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

onOpenExternal: (url: string) => Promise<boolean>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
onOpenExternal: (url: string) => Promise<boolean>
onOpenExternal?: (url: string) => Promise<boolean>

}

export interface ClientApi extends ClientApiMethods {
Expand Down Expand Up @@ -119,6 +120,7 @@ export function createClient(target: HTMLIFrameElement, api: ClientApiMethods):
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 @@ -557,6 +557,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 @@ -33,6 +33,7 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi
onLoaded: api.onLoaded,
onCopy: api.onCopy,
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 @@

/// 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
21 changes: 20 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 @@ -74,6 +74,7 @@ export default function ChatPage() {
// server feature support check
const [supportsOnApplyInEditorV2, setSupportsOnApplyInEditorV2] =
useState(false)
const [supportsOnOpenExternal, setSupportsOnOpenExternal] = useState(false)

const sendMessage = (message: ChatMessage) => {
if (chatRef.current) {
Expand Down Expand Up @@ -236,6 +237,8 @@ export default function ChatPage() {
server
?.hasCapability('onApplyInEditorV2')
.then(setSupportsOnApplyInEditorV2)

server?.hasCapability('onOpenExternal').then(setSupportsOnOpenExternal)
}

checkCapabilities()
Expand Down Expand Up @@ -275,6 +278,21 @@ export default function ChatPage() {
server?.navigate(context, opts)
}

const onOpenExternal = useMemo(() => {
if (isInEditor && !supportsOnOpenExternal) {
return undefined
}

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

const refresh = async () => {
setIsRefreshLoading(true)
await server?.refresh()
Expand Down Expand Up @@ -388,6 +406,7 @@ export default function ChatPage() {
: server?.onApplyInEditor)
}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
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 @@ -949,12 +950,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
10 changes: 8 additions & 2 deletions 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 = {
removeRelevantContext: (index: number) => void
chatInputRef: RefObject<HTMLTextAreaElement>
supportsOnApplyInEditorV2: boolean
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'> {
| ((content: string, opts?: { languageId: string; smart: boolean }) => void)
chatInputRef: RefObject<HTMLTextAreaElement>
supportsOnApplyInEditorV2: boolean
onOpenExternal?: (url: string) => Promise<boolean | undefined>
}

function ChatRenderer(
Expand All @@ -105,7 +108,8 @@ function ChatRenderer(
onSubmitMessage,
onApplyInEditor,
chatInputRef,
supportsOnApplyInEditorV2
supportsOnApplyInEditorV2,
onOpenExternal
}: ChatProps,
ref: React.ForwardedRef<ChatRef>
) {
Expand Down Expand Up @@ -521,6 +525,7 @@ function ChatRenderer(
return (
<ChatContext.Provider
value={{
threadId,
isLoading,
qaPairs,
onNavigateToContext,
Expand All @@ -533,7 +538,8 @@ function ChatRenderer(
removeRelevantContext,
chatInputRef,
activeSelection,
supportsOnApplyInEditorV2
supportsOnApplyInEditorV2,
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)
}
`)
Loading