Skip to content

Commit

Permalink
feat(ui): add chat panel to code browser (TabbyML#1748)
Browse files Browse the repository at this point in the history
* call action

* rebase

* action

* iframe

* [autofix.ci] apply automated fixes

* toggle panel icon

* [autofix.ci] apply automated fixes

* language

* [autofix.ci] apply automated fixes

* breadcrumb style

* ask tabby

* [autofix.ci] apply automated fixes

* clear and rename

* nanoid

* remove useless icons

* remove useless props

* reset

* [autofix.ci] apply automated fixes

* remove

* trigger visible

* cleanup

* revert

* cleanup

* revert

* revert

* ResizableHandle bg

* breadcrumb style

* [autofix.ci] apply automated fixes

* header bg

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Apr 2, 2024
1 parent 955089b commit 206afe9
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import { ActionBarWidget } from './action-bar-widget'

let delayTimer: number

function ActionBarWidgetExtension(): Extension {
interface Options {
language?: string
}

function ActionBarWidgetExtension(options: Options): Extension {
return StateField.define<Tooltip | null>({
create() {
return null
Expand All @@ -20,9 +24,7 @@ function ActionBarWidgetExtension(): Extension {
}
if (transaction.selection) {
if (shouldShowActionBarWidget(transaction)) {
const tooltip = createActionBarWidget(transaction.state)
// avoid flickering
// return tooltip?.pos !== value?.pos ? tooltip : value
const tooltip = createActionBarWidget(transaction.state, options)
return tooltip
}

Expand All @@ -35,12 +37,15 @@ function ActionBarWidgetExtension(): Extension {
})
}

function createActionBarWidget(state: EditorState): Tooltip {
function createActionBarWidget(state: EditorState, options: Options): Tooltip {
const { selection } = state
const lineFrom = state.doc.lineAt(selection.main.from)
const lineTo = state.doc.lineAt(selection.main.to)
const isMultiline = lineFrom.number !== lineTo.number
const pos = isMultiline ? lineTo.from : selection.main.from
const text =
state.doc.sliceString(state.selection.main.from, state.selection.main.to) ||
''

return {
pos,
Expand All @@ -56,7 +61,9 @@ function createActionBarWidget(state: EditorState): Tooltip {
// delay popup
if (delayTimer) clearTimeout(delayTimer)
delayTimer = window.setTimeout(() => {
root.render(<ActionBarWidget />)
root.render(
<ActionBarWidget text={text} language={options?.language} />
)
}, 1000)

return { dom }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ import { IconChevronUpDown } from '@/components/ui/icons'

import { CodeBrowserQuickAction, emitter } from '../../lib/event-emitter'

interface ActionBarWidgetProps extends React.HTMLAttributes<HTMLDivElement> {}
interface ActionBarWidgetProps extends React.HTMLAttributes<HTMLDivElement> {
text: string
language?: string
}

export const ActionBarWidget: React.FC<ActionBarWidgetProps> = ({
className,
text,
language,
...props
}) => {
const handleAction = (action: CodeBrowserQuickAction) => {
emitter.emit('code_browser_quick_action', action)
emitter.emit('code_browser_quick_action', { action, code: text, language })
}

return (
Expand Down
38 changes: 34 additions & 4 deletions ee/tabby-ui/app/files/components/blob-header.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
'use client'

import React from 'react'
import Image from 'next/image'
import tabbyLogo from '@/assets/tabby.png'
import { isNil } from 'lodash-es'
import prettyBytes from 'pretty-bytes'
import { toast } from 'sonner'

import { useEnableCodeBrowserQuickActionBar } from '@/lib/experiment-flags'
import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard'
import { useIsSticky } from '@/lib/hooks/use-is-sticky'
import { useIsChatEnabled } from '@/lib/hooks/use-server-info'
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons'
Expand Down Expand Up @@ -38,10 +42,25 @@ export const BlobHeader: React.FC<BlobHeaderProps> = ({
children,
...props
}) => {
const isChatEnabled = useIsChatEnabled()
const { chatSideBarVisible, setChatSideBarVisible } = React.useContext(
SourceCodeBrowserContext
)
const [enableCodeBrowserQuickActionBar] = useEnableCodeBrowserQuickActionBar()
const containerRef = React.useRef<HTMLDivElement>(null)
const { activePath } = React.useContext(SourceCodeBrowserContext)
const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
const isSticky = useIsSticky(containerRef)

const showChatPanelTrigger =
isChatEnabled &&
enableCodeBrowserQuickActionBar.value &&
!chatSideBarVisible

const contentLengthText = !isNil(contentLength)
? prettyBytes(contentLength)
: ''

const onCopy: React.MouseEventHandler<HTMLButtonElement> = async () => {
if (isCopied || !blob) return
try {
Expand All @@ -52,10 +71,6 @@ export const BlobHeader: React.FC<BlobHeaderProps> = ({
}
}

const contentLengthText = !isNil(contentLength)
? prettyBytes(contentLength)
: ''

return (
<div
className={cn(
Expand Down Expand Up @@ -120,6 +135,21 @@ export const BlobHeader: React.FC<BlobHeaderProps> = ({
<TooltipContent>Download raw file</TooltipContent>
</Tooltip>
)}
{showChatPanelTrigger && (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className="flex shrink-0 items-center gap-1 px-2"
onClick={e => setChatSideBarVisible(!chatSideBarVisible)}
>
<Image alt="Tabby logo" src={tabbyLogo} width={24} />
Ask Tabby
</Button>
</TooltipTrigger>
<TooltipContent>Open chat panel</TooltipContent>
</Tooltip>
)}
</div>
</div>
)}
Expand Down
82 changes: 82 additions & 0 deletions ee/tabby-ui/app/files/components/chat-side-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react'

import { useStore } from '@/lib/hooks/use-store'
import { useChatStore } from '@/lib/stores/chat-store'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
import { IconClose } from '@/components/ui/icons'

import { QuickActionEventPayload } from '../lib/event-emitter'
import { SourceCodeBrowserContext } from './source-code-browser'

interface ChatSideBarProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {}

export const ChatSideBar: React.FC<ChatSideBarProps> = ({
className,
...props
}) => {
const { pendingEvent, setPendingEvent } = React.useContext(
SourceCodeBrowserContext
)
const activeChatId = useStore(useChatStore, state => state.activeChatId)
const iframeRef = React.useRef<HTMLIFrameElement>(null)

const getPrompt = ({ action, code, language }: QuickActionEventPayload) => {
let builtInPrompt = ''
switch (action) {
case 'explain':
builtInPrompt = 'Explain the following code:'
break
case 'generate_unittest':
builtInPrompt = 'Generate a unit test for the following code:'
break
case 'generate_doc':
builtInPrompt = 'Generate documentation for the following code:'
break
default:
break
}
return `${builtInPrompt}\n${'```'}${language ?? ''}\n${code}\n${'```'}\n`
}

React.useEffect(() => {
const contentWindow = iframeRef.current?.contentWindow

if (pendingEvent) {
contentWindow?.postMessage({
action: 'append',
payload: getPrompt(pendingEvent)
})
setPendingEvent(undefined)
}
}, [pendingEvent, iframeRef.current?.contentWindow])

return (
<div className={cn('flex h-full flex-col', className)} {...props}>
<Header />
<iframe
src={`/playground`}
className="w-full flex-1 border-0"
key={activeChatId}
ref={iframeRef}
/>
</div>
)
}

function Header() {
const { setChatSideBarVisible } = React.useContext(SourceCodeBrowserContext)

return (
<div className="sticky top-0 flex items-center justify-end px-2 py-1">
<Button
size="icon"
variant="ghost"
onClick={e => setChatSideBarVisible(false)}
>
<IconClose />
</Button>
</div>
)
}
45 changes: 2 additions & 43 deletions ee/tabby-ui/app/files/components/code-editor-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { markTagNameExtension } from '@/components/codemirror/name-tag-extension
import { highlightTagExtension } from '@/components/codemirror/tag-range-highlight-extension'
import { codeTagHoverTooltip } from '@/components/codemirror/tooltip-extesion'

import { CodeBrowserQuickAction, emitter } from '../lib/event-emitter'
import { ActionBarWidgetExtension } from './action-bar-widget/action-bar-widget-extension'

interface CodeEditorViewProps {
Expand Down Expand Up @@ -50,7 +49,7 @@ const CodeEditorView: React.FC<CodeEditorViewProps> = ({
drawSelection()
]
if (EXP_enable_code_browser_quick_action_bar.value && isChatEnabled) {
result.push(ActionBarWidgetExtension())
result.push(ActionBarWidgetExtension({ language }))
}
if (value && tags) {
result.push(
Expand All @@ -60,47 +59,7 @@ const CodeEditorView: React.FC<CodeEditorViewProps> = ({
)
}
return result
}, [value, tags, editorRef.current])

React.useEffect(() => {
const quickActionBarCallback = (action: CodeBrowserQuickAction) => {
let builtInPrompt = ''
switch (action) {
case 'explain':
builtInPrompt = 'Explain the following code:'
break
case 'generate_unittest':
builtInPrompt = 'Generate a unit test for the following code:'
break
case 'generate_doc':
builtInPrompt = 'Generate documentation for the following code:'
break
default:
break
}
const view = editorRef.current?.editorView
const text =
view?.state.doc.sliceString(
view?.state.selection.main.from,
view?.state.selection.main.to
) || ''

const initialMessage = `${builtInPrompt}\n${'```'}${
language ?? ''
}\n${text}\n${'```'}\n`
if (initialMessage) {
window.open(
`/playground?initialMessage=${encodeURIComponent(initialMessage)}`
)
}
}

emitter.on('code_browser_quick_action', quickActionBarCallback)

return () => {
emitter.off('code_browser_quick_action', quickActionBarCallback)
}
}, [])
}, [value, tags, language, editorRef.current])

return (
<CodeEditor
Expand Down
65 changes: 33 additions & 32 deletions ee/tabby-ui/app/files/components/file-directory-breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,41 @@ const FileDirectoryBreadcrumb: React.FC<FileDirectoryBreadcrumbProps> = ({
)

return (
<div
className={cn('flex flex-nowrap items-center gap-1 leading-8', className)}
>
<div
className="cursor-pointer font-medium text-primary hover:underline"
onClick={e => setActivePath(undefined)}
>
Repositories
</div>
<div>/</div>
{currentFileRoutes?.map((route, idx) => {
const isRepo = idx === 0 && currentFileRoutes?.length > 1
const isActiveFile = idx === currentFileRoutes.length - 1
<div className={cn('flex flex-nowrap items-center gap-1', className)}>
<div className="flex items-center gap-1 overflow-x-auto leading-8">
<div
className="cursor-pointer font-medium text-primary hover:underline"
onClick={e => setActivePath(undefined)}
>
Repositories
</div>
<div>/</div>
{currentFileRoutes?.map((route, idx) => {
const isRepo = idx === 0 && currentFileRoutes?.length > 1
const isActiveFile = idx === currentFileRoutes.length - 1

return (
<React.Fragment key={route.fullPath}>
<div
className={cn(
isRepo || isActiveFile ? 'font-bold' : 'font-medium',
isActiveFile
? ''
: 'cursor-pointer text-primary hover:underline',
isRepo ? 'hover:underline' : undefined
)}
onClick={e => setActivePath(route.fullPath)}
>
{route.name}
</div>
{route.file.kind !== 'file' && <div>/</div>}
</React.Fragment>
)
})}
return (
<React.Fragment key={route.fullPath}>
<div
className={cn(
'whitespace-nowrap',
isRepo || isActiveFile ? 'font-bold' : 'font-medium',
isActiveFile
? ''
: 'cursor-pointer text-primary hover:underline',
isRepo ? 'hover:underline' : undefined
)}
onClick={e => setActivePath(route.fullPath)}
>
{route.name}
</div>
{route.file.kind !== 'file' && <div>/</div>}
</React.Fragment>
)
})}
</div>
{!!currentFileRoutes?.length && !!activePath && (
<CopyButton value={activePath} />
<CopyButton className="shrink-0" value={activePath} />
)}
</div>
)
Expand Down
Loading

0 comments on commit 206afe9

Please sign in to comment.