Skip to content

Commit

Permalink
Qurator AI Assistant for summarizing file contents using Bedrock API (#…
Browse files Browse the repository at this point in the history
…3989)

Co-authored-by: Alexei Mochalov <[email protected]>
  • Loading branch information
fiskus and nl0 authored Jun 19, 2024
1 parent 15fcd5a commit c3dc6b0
Show file tree
Hide file tree
Showing 27 changed files with 941 additions and 26 deletions.
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

0 comments on commit c3dc6b0

Please sign in to comment.