Skip to content

Commit

Permalink
add new actions to ai chat (#1731)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsladerman authored Jan 8, 2025
1 parent 780af00 commit b094ebf
Show file tree
Hide file tree
Showing 10 changed files with 494 additions and 130 deletions.
2 changes: 1 addition & 1 deletion assets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@nivo/radial-bar": "0.88.0",
"@nivo/tooltip": "0.88.0",
"@nivo/treemap": "0.88.0",
"@pluralsh/design-system": "5.0.0",
"@pluralsh/design-system": "5.0.1",
"@react-hooks-library/core": "0.6.0",
"@saas-ui/use-hotkeys": "1.1.3",
"@tanstack/react-table": "8.20.5",
Expand Down
216 changes: 180 additions & 36 deletions assets/src/components/ai/chatbot/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import {
Accordion,
AccordionItem,
AppIcon,
ArrowTopRightIcon,
Card,
CheckIcon,
Code,
CopyIcon,
FileIcon,
Flex,
GitHubLogoIcon,
IconFrame,
Markdown,
PluralLogoMark,
Expand All @@ -15,79 +22,157 @@ import { ComponentProps, ReactNode, useState } from 'react'
import styled, { CSSObject, useTheme } from 'styled-components'
import { aiGradientBorderStyles } from '../explain/ExplainWithAIButton'

import { AiRole, useDeleteChatMutation } from 'generated/graphql'
import { Body2BoldP, CaptionP } from 'components/utils/typography/Text'
import {
AiRole,
ChatType,
ChatTypeAttributes,
PullRequestFragment,
useDeleteChatMutation,
} from 'generated/graphql'
import CopyToClipboard from 'react-copy-to-clipboard'

export function ChatMessage({
id,
content,
role,
type = ChatType.Text,
attributes,
pullRequest,
disableActions,
contentStyles,
...props
}: {
id?: string
content: string
role: AiRole
type?: ChatType
attributes?: Nullable<ChatTypeAttributes>
pullRequest?: Nullable<PullRequestFragment>
disableActions?: boolean
contentStyles?: CSSObject
} & ComponentProps<typeof ChatMessageSC>) {
const theme = useTheme()
} & Omit<ComponentProps<typeof ChatMessageSC>, '$role'>) {
const [showActions, setShowActions] = useState(false)
let finalContent: ReactNode

if (role === AiRole.Assistant || role === AiRole.System) {
finalContent = <Markdown text={content} />
} else {
finalContent = content.split('\n\n').map((str, i) => (
<Card
key={i}
css={{ padding: theme.spacing.medium }}
fillLevel={2}
>
{str.split('\n').map((line, i, arr) => (
<div
key={`${i}-${line}`}
css={{ display: 'contents' }}
>
{line}
{i !== arr.length - 1 ? <br /> : null}
</div>
))}
</Card>
))
finalContent = (
<ChatMessageContent
id={id ?? ''}
showActions={showActions && !disableActions}
content={content}
type={type}
attributes={attributes}
/>
)
}

return (
return pullRequest ? (
<PrLinkout pullRequest={pullRequest} />
) : (
<ChatMessageSC
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
$role={role}
{...props}
>
<ChatMessageActions
id={id ?? ''}
content={content}
show={showActions && !disableActions}
/>
<Flex
gap="medium"
justify={role === AiRole.User ? 'flex-end' : 'flex-start'}
>
{role !== AiRole.User && <PluralAssistantIcon />}
<div css={{ overflow: 'hidden', ...contentStyles }}>{finalContent}</div>
<div
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
css={{ overflow: 'hidden', ...contentStyles }}
>
{finalContent}
<ChatMessageActions
id={id ?? ''}
content={content}
show={showActions && type !== ChatType.File && !disableActions}
/>
</div>
</Flex>
</ChatMessageSC>
)
}

function ChatMessageActions({
function ChatMessageContent({
id,
showActions,
content,
show,
type,
attributes,
}: {
id: string
showActions: boolean
content: string
show: boolean
type: ChatType
attributes?: Nullable<ChatTypeAttributes>
}) {
const theme = useTheme()
const fileName = attributes?.file?.name ?? ''
return type === ChatType.File ? (
<Accordion type="single">
<AccordionItem
padding="compact"
caret="right"
trigger={
<Flex
gap="small"
align="center"
wordBreak="break-word"
marginRight={theme.spacing.small}
>
<FileIcon
size={12}
color="icon-light"
/>
<CaptionP $color="text-light">{fileName || 'File'}</CaptionP>
<ChatMessageActions
id={id}
content={fileName}
show={showActions}
/>
</Flex>
}
>
<Code
css={{ background: theme.colors['fill-three'], maxWidth: '100%' }}
>
{content}
</Code>
</AccordionItem>
</Accordion>
) : (
<Card
css={{ padding: theme.spacing.medium }}
fillLevel={2}
>
{content.split('\n').map((line, i, arr) => (
<div
key={`${i}-${line}`}
css={{ display: 'contents' }}
>
{line}
{i !== arr.length - 1 ? <br /> : null}
</div>
))}
</Card>
)
}

function ChatMessageActions({
id,
content,
show = true,
...props
}: {
id: string
content: string
show?: boolean
} & Omit<ComponentProps<typeof ActionsWrapperSC>, '$show'>) {
const [copied, setCopied] = useState(false)

const showCopied = () => {
Expand All @@ -101,7 +186,11 @@ function ChatMessageActions({
})

return (
<ActionsWrapperSC $show={show}>
<ActionsWrapperSC
onClick={(e) => e.stopPropagation()}
$show={show}
{...props}
>
<WrapWithIf
condition={!copied}
wrapper={
Expand All @@ -113,6 +202,7 @@ function ChatMessageActions({
>
<IconFrame
clickable
as="div"
tooltip="Copy to clipboard"
type="floating"
size="medium"
Expand All @@ -121,6 +211,7 @@ function ChatMessageActions({
</WrapWithIf>
<IconFrame
clickable
as="div"
tooltip="Delete message"
type="floating"
size="medium"
Expand All @@ -135,20 +226,73 @@ function ChatMessageActions({
)
}

function PrLinkout({ pullRequest }: { pullRequest: PullRequestFragment }) {
const theme = useTheme()
return (
<Flex
paddingLeft={theme.spacing.xxxlarge}
paddingRight={theme.spacing.xxxlarge}
paddingTop={theme.spacing.small}
paddingBottom={theme.spacing.small}
direction="column"
gap="xsmall"
>
<CaptionP $color="text-light">PR generated from chat context</CaptionP>
<Card
clickable
onClick={() => {
window.open(pullRequest.url, '_blank')
}}
css={{
padding: `${theme.spacing.small}px ${theme.spacing.large}px`,
width: '100%',
}}
>
<Flex
justify="space-between"
align="center"
>
<Flex
gap="small"
align="center"
>
<AppIcon
icon={<GitHubLogoIcon size={32} />}
size="xsmall"
/>
<Body2BoldP $color="text-light">{pullRequest.title}</Body2BoldP>
</Flex>
<ArrowTopRightIcon
color="icon-light"
size={20}
/>
</Flex>
</Card>
</Flex>
)
}

const ActionsWrapperSC = styled.div<{ $show: boolean }>(({ theme, $show }) => ({
position: 'absolute',
top: theme.spacing.small,
zIndex: theme.zIndexes.tooltip,
top: 4,
right: theme.spacing.small,
display: 'flex',
gap: theme.spacing.xsmall,
opacity: $show ? 1 : 0,
transition: '0.2s opacity ease',
transition: '0.3s opacity ease',
pointerEvents: $show ? 'auto' : 'none',
}))

const ChatMessageSC = styled.div(({ theme }) => ({
const ChatMessageSC = styled.div<{ $role: AiRole }>(({ theme, $role }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.xsmall,
position: 'relative',
padding: theme.spacing.small,
paddingBottom: $role === AiRole.Assistant ? theme.spacing.small : 0,
width: '100%',
justifySelf: $role === AiRole.User ? 'flex-end' : 'flex-start',
}))

function PluralAssistantIcon() {
Expand Down
3 changes: 3 additions & 0 deletions assets/src/components/ai/chatbot/ChatbotPanelThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ChatThreadDetailsDocument,
ChatThreadDetailsQuery,
ChatThreadFragment,
ChatType,
useAiChatStreamSubscription,
useChatMutation,
useChatThreadDetailsQuery,
Expand Down Expand Up @@ -81,6 +82,7 @@ export function ChatbotPanelThread({
content: messages?.[0]?.content ?? '',
role: AiRole.User,
seq: 0,
type: ChatType.Text,
insertedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
Expand Down Expand Up @@ -157,6 +159,7 @@ export function ChatbotPanelThread({
))}
</ChatbotMessagesWrapper>
<SendMessageForm
currentThread={currentThread}
sendMessage={sendMessage}
fullscreen={fullscreen}
/>
Expand Down
Loading

0 comments on commit b094ebf

Please sign in to comment.