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

add new actions to ai chat #1731

Merged
merged 1 commit into from
Jan 8, 2025
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 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
Loading