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

Qurator AI Assistant for summarizing file contents using Bedrock API #3989

Merged
merged 9 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
91 changes: 91 additions & 0 deletions catalog/app/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import * as React from 'react'
import * as M from '@material-ui/core'
import * as Lab from '@material-ui/lab'

import Skeleton from 'components/Skeleton'
import type * as AWS from 'utils/AWS'

import History from './History'
import Input from './Input'

const useStyles = M.makeStyles((t) => ({
root: {
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
},
error: {
marginTop: t.spacing(2),
},
history: {
...t.typography.body1,
maxHeight: t.spacing(70),
overflowY: 'auto',
},
input: {
marginTop: t.spacing(2),
},
}))

const noMessages: AWS.Bedrock.Message[] = []

export function ChatSkeleton() {
const classes = useStyles()
return (
<div className={classes.root}>
<History loading messages={noMessages} />
<Skeleton className={classes.input} height="32px" />
</div>
)
}

const Submitting = Symbol('Submitting')

interface ChatProps {
initializing: boolean
history: AWS.Bedrock.History
onSubmit: (value: string) => Promise<void>
}

export default function Chat({ history, onSubmit, initializing }: ChatProps) {
const classes = useStyles()

const [value, setValue] = React.useState('')
const [state, setState] = React.useState<Error | typeof Submitting | null>(null)

const handleSubmit = React.useCallback(async () => {
if (state) return

setState(Submitting)
try {
await onSubmit(value)
setValue('')
} catch (e) {
setState(e instanceof Error ? e : new Error('Failed to submit message'))
}
setState(null)
}, [state, onSubmit, value])

return (
<div className={classes.root}>
<History
className={classes.history}
loading={state === Submitting || initializing}
messages={history.messages}
/>
{state instanceof Error && (
<Lab.Alert className={classes.error} severity="error">
{state.message}
</Lab.Alert>
)}
<Input
className={classes.input}
disabled={state === Submitting}
onChange={setValue}
onSubmit={handleSubmit}
value={value}
/>
</div>
)
}
101 changes: 101 additions & 0 deletions catalog/app/components/Chat/History.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import cx from 'classnames'
import * as React from 'react'
import * as M from '@material-ui/core'

import usePrevious from 'utils/usePrevious'
import * as AWS from 'utils/AWS'

import * as Messages from './Message'

const useStyles = M.makeStyles((t) => ({
assistant: {
animation: `$show 300ms ease-out`,
},
message: {
'& + &': {
marginTop: t.spacing(2),
},
},
user: {
animation: `$slide 150ms ease-out`,
marginLeft: 'auto',
width: '60%',
},
'@keyframes slide': {
'0%': {
transform: `translateX($${t.spacing(8)}px)`,
},
'100%': {
transform: `translateX(0)`,
},
},
'@keyframes show': {
'0%': {
opacity: 0.7,
},
'100%': {
opacity: '1',
},
},
}))

interface HistoryProps {
className?: string
loading: boolean
messages: AWS.Bedrock.Message[]
}

export default function History({ className, loading, messages }: HistoryProps) {
const classes = useStyles()

const list = React.useMemo(
() => messages.filter((message) => message.role !== 'system'),
[messages],
)

const ref = React.useRef<HTMLDivElement | null>(null)
usePrevious(messages, (prev) => {
if (prev && messages.length > prev.length) {
ref.current?.scroll({
top: ref.current?.firstElementChild?.clientHeight,
behavior: 'smooth',
})
}
})

return (
<div className={className} ref={ref}>
<div /* full height scroll area */>
{list.map((message, index) => {
switch (message.role) {
case 'user':
return (
<Messages.User
key={`message_${index}`}
className={cx(classes.message, classes.user)}
content={message.content}
/>
)
case 'summarize':
return (
<Messages.User
key={`message_${index}`}
className={cx(classes.message, classes.user)}
content="Summarize this document"
/>
)
case 'assistant':
return (
<Messages.Assistant
key={`message_${index}`}
className={cx(classes.message, classes.assistant)}
content={message.content}
/>
)
}
})}
{loading && <Messages.Skeleton className={classes.message} />}
</div>
</div>
)
}
51 changes: 51 additions & 0 deletions catalog/app/components/Chat/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from 'react'
import * as M from '@material-ui/core'

interface ChatInputProps {
className?: string
disabled?: boolean
onChange: (value: string) => void
onSubmit: () => void
value: string
}

export default function ChatInput({
className,
disabled,
onChange,
onSubmit,
value,
}: ChatInputProps) {
const handleSubmit = React.useCallback(
(event) => {
event.preventDefault()
if (!value || disabled) return
onSubmit()
},
[disabled, onSubmit, value],
)
return (
<form onSubmit={handleSubmit}>
<M.TextField
autoFocus
className={className}
disabled={disabled}
fullWidth
label="Chat"
onChange={(e) => onChange(e.target.value)}
size="small"
value={value}
variant="outlined"
InputProps={{
endAdornment: (
<M.InputAdornment position="end">
<M.IconButton disabled={!value} onClick={onSubmit} type="submit">
<M.Icon>send</M.Icon>
</M.IconButton>
</M.InputAdornment>
),
}}
/>
</form>
)
}
73 changes: 73 additions & 0 deletions catalog/app/components/Chat/Message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import cx from 'classnames'
import * as React from 'react'
import * as M from '@material-ui/core'
import { fade } from '@material-ui/core/styles'

import Markdown from 'components/Markdown'
import Skel from 'components/Skeleton'

const useSkeletonStyles = M.makeStyles((t) => ({
text: {
height: t.spacing(2),
'& + &': {
marginTop: t.spacing(1),
},
},
}))

interface SkeletonProps {
className?: string
}

export function Skeleton({ className }: SkeletonProps) {
const classes = useSkeletonStyles()
return (
<div className={className}>
<Skel className={classes.text} width="30%" />
<Skel className={classes.text} width="90%" />
<Skel className={classes.text} width="70%" />
<Skel className={classes.text} width="50%" />
</div>
)
}

interface AssistantProps {
className?: string
content: string
}

export function Assistant({ className, content }: AssistantProps) {
return (
<div className={className}>
<Markdown data={content} />
</div>
)
}

const useUserStyles = M.makeStyles((t) => ({
root: {
borderRadius: t.shape.borderRadius,
background: t.palette.primary.main,
},
inner: {
padding: t.spacing(2),
background: fade(t.palette.background.paper, 0.9),
},
}))

interface UserProps {
className?: string
content: string
}

export function User({ className, content }: UserProps) {
const classes = useUserStyles()

return (
<div className={cx(classes.root, className)}>
<div className={classes.inner}>
<Markdown data={content} />
</div>
</div>
)
}
3 changes: 3 additions & 0 deletions catalog/app/components/Chat/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as Input } from './Input'
export { default as History } from './History'
export { default, ChatSkeleton } from './Chat'
12 changes: 12 additions & 0 deletions catalog/app/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ const useContainerStyles = M.makeStyles({
maxWidth: '100%',
},

'& * + h1, & * + h2, & * + h3, & * + h4, & * + h5, & * + h6': {
marginTop: '8px',
},

'& * + p': {
marginTop: '8px',
},

'& li + li': {
marginTop: '4px',
},

'& table': {
maxWidth: '100%',
width: '100%',
Expand Down
4 changes: 4 additions & 0 deletions catalog/app/containers/Bucket/File.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { readableBytes, readableQuantity } from 'utils/string'
import FileCodeSamples from './CodeSamples/File'
import FileProperties from './FileProperties'
import * as FileView from './FileView'
import QuratorSection from './Qurator/Section'
import Section from './Section'
import renderPreview from './renderPreview'
import * as requests from './requests'
Expand Down Expand Up @@ -501,6 +502,9 @@ export default function File() {
{!!cfg.analyticsBucket && !!blocks.analytics && (
<Analytics {...{ bucket, path }} />
)}
{cfg.qurator && blocks.qurator && (
<QuratorSection handle={handle} />
)}
{blocks.meta && (
<>
<FileView.ObjectMeta handle={handle} />
Expand Down
4 changes: 4 additions & 0 deletions catalog/app/containers/Bucket/PackageTree/PackageTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import * as FileView from '../FileView'
import * as Listing from '../Listing'
import PackageCopyDialog from '../PackageCopyDialog'
import * as PD from '../PackageDialog'
import QuratorSection from '../Qurator/Section'
import Section from '../Section'
import * as Successors from '../Successors'
import Summary from '../Summary'
Expand Down Expand Up @@ -677,6 +678,9 @@ function FileDisplay({
<FileView.ObjectTags handle={handle} />
</>
)}
{cfg.qurator && blocks.qurator && (
<QuratorSection handle={handle} />
)}
</>
),
_: () => null,
Expand Down
Loading
Loading