diff --git a/.github/workflows/py-ci.yml b/.github/workflows/py-ci.yml index 2078d525415..5e3b2f3961b 100644 --- a/.github/workflows/py-ci.yml +++ b/.github/workflows/py-ci.yml @@ -181,16 +181,23 @@ jobs: python-version-file: lambdas/${{ matrix.path }}/.python-version - name: Install dependencies run: | - # Due to behavior change in pip>=23.1 installing tifffile==0.15.1 - # from thumbnail lambda fails whithout installed wheel. - # See https://github.com/pypa/pip/issues/8559. - python -m pip install wheel + if [ ${{ matrix.path }} == "thumbnail" ] + then + # Due to behavior change in pip>=23.1 installing tifffile==0.15.1 + # from thumbnail lambda fails whithout installed wheel. + # See https://github.com/pypa/pip/issues/8559. + # HACK: Pre-install numpy v1 as a build dependency for tifffile to prevent it from using v2 and failing to build + python -m pip install wheel 'numpy<2' + fi + + # XXX: something fishy is going on with this "if [ ] X then Y" syntax if [ ${{ matrix.path }} == "shared" ] python -m pip install -e lambdas/shared[tests] then python -m pip install -e lambdas/shared python -m pip install -e lambdas/${{ matrix.path }} fi + python -m pip install -r lambdas/${{ matrix.path }}/test-requirements.txt # Try to simulate the lambda .zip file: diff --git a/api/python/quilt3/util.py b/api/python/quilt3/util.py index 6fa8390bcda..153e3cc7529 100644 --- a/api/python/quilt3/util.py +++ b/api/python/quilt3/util.py @@ -13,7 +13,7 @@ urlparse, urlunparse, ) -from urllib.request import pathname2url, url2pathname +from urllib.request import url2pathname import requests # Third-Party @@ -222,7 +222,7 @@ def __repr__(self): def __str__(self): if self.bucket is None: - return urlunparse(('file', '', pathname2url(self.path.replace('/', os.path.sep)), None, None, None)) + return pathlib.PurePath(self.path).as_uri() else: if self.version_id is None: params = {} diff --git a/catalog/Dockerfile b/catalog/Dockerfile index e3cd2e97ec9..ac993d911e3 100644 --- a/catalog/Dockerfile +++ b/catalog/Dockerfile @@ -1,4 +1,4 @@ -FROM amazonlinux:2023.4.20240528.0 +FROM amazonlinux:2023.4.20240611.0 MAINTAINER Quilt Data, Inc. contact@quiltdata.io ENV LC_ALL=C.UTF-8 diff --git a/catalog/app/app.tsx b/catalog/app/app.tsx index 4caf240de51..ae417dfac70 100644 --- a/catalog/app/app.tsx +++ b/catalog/app/app.tsx @@ -22,7 +22,6 @@ Sentry.init(cfg, history) import 'sanitize.css' // Import the rest of our modules -import { ExperimentsProvider } from 'components/Experiments' import * as Intercom from 'components/Intercom' import Placeholder from 'components/Placeholder' import App from 'containers/App' @@ -39,6 +38,7 @@ import * as APIConnector from 'utils/APIConnector' import * as GraphQL from 'utils/GraphQL' import { BucketCacheProvider } from 'utils/BucketCache' import GlobalAPI from 'utils/GlobalAPI' +import WithGlobalDialogs from 'utils/GlobalDialogs' import log from 'utils/Logging' import * as NamedRoutes from 'utils/NamedRoutes' import { PFSCookieManager } from 'utils/PFSCookieManager' @@ -116,13 +116,13 @@ const render = () => { vertical_padding: 59, }, ], - ExperimentsProvider, [Tracking.Provider, { userSelector: Auth.selectors.username }], AWS.Credentials.Provider, AWS.Config.Provider, AWS.Athena.Provider, AWS.S3.Provider, Notifications.WithNotifications, + WithGlobalDialogs, Errors.ErrorBoundary, BucketCacheProvider, PFSCookieManager, diff --git a/catalog/app/components/Chat/Chat.tsx b/catalog/app/components/Chat/Chat.tsx new file mode 100644 index 00000000000..29222aa5926 --- /dev/null +++ b/catalog/app/components/Chat/Chat.tsx @@ -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 ( +
+ + +
+ ) +} + +const Submitting = Symbol('Submitting') + +interface ChatProps { + initializing: boolean + history: AWS.Bedrock.History + onSubmit: (value: string) => Promise +} + +export default function Chat({ history, onSubmit, initializing }: ChatProps) { + const classes = useStyles() + + const [value, setValue] = React.useState('') + const [state, setState] = React.useState(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 ( +
+ + {state instanceof Error && ( + + {state.message} + + )} + +
+ ) +} diff --git a/catalog/app/components/Chat/History.tsx b/catalog/app/components/Chat/History.tsx new file mode 100644 index 00000000000..bb1ef818756 --- /dev/null +++ b/catalog/app/components/Chat/History.tsx @@ -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(null) + usePrevious(messages, (prev) => { + if (prev && messages.length > prev.length) { + ref.current?.scroll({ + top: ref.current?.firstElementChild?.clientHeight, + behavior: 'smooth', + }) + } + }) + + return ( +
+
+ {list.map((message, index) => { + switch (message.role) { + case 'user': + return ( + + ) + case 'summarize': + return ( + + ) + case 'assistant': + return ( + + ) + } + })} + {loading && } +
+
+ ) +} diff --git a/catalog/app/components/Chat/Input.tsx b/catalog/app/components/Chat/Input.tsx new file mode 100644 index 00000000000..f1524ccce94 --- /dev/null +++ b/catalog/app/components/Chat/Input.tsx @@ -0,0 +1,52 @@ +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 ( +
+ onChange(e.target.value)} + size="small" + value={value} + variant="outlined" + InputProps={{ + endAdornment: ( + + + send + + + ), + }} + /> + + ) +} diff --git a/catalog/app/components/Chat/Message.tsx b/catalog/app/components/Chat/Message.tsx new file mode 100644 index 00000000000..390a265ae1f --- /dev/null +++ b/catalog/app/components/Chat/Message.tsx @@ -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 ( +
+ + + + +
+ ) +} + +interface AssistantProps { + className?: string + content: string +} + +export function Assistant({ className, content }: AssistantProps) { + return ( +
+ +
+ ) +} + +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 ( +
+
+ +
+
+ ) +} diff --git a/catalog/app/components/Chat/index.ts b/catalog/app/components/Chat/index.ts new file mode 100644 index 00000000000..ef4f2666527 --- /dev/null +++ b/catalog/app/components/Chat/index.ts @@ -0,0 +1,3 @@ +export { default as Input } from './Input' +export { default as History } from './History' +export { default, ChatSkeleton } from './Chat' diff --git a/catalog/app/components/Dialog/Confirm.tsx b/catalog/app/components/Dialog/Confirm.tsx index 15d1749d709..0146c0e071c 100644 --- a/catalog/app/components/Dialog/Confirm.tsx +++ b/catalog/app/components/Dialog/Confirm.tsx @@ -44,6 +44,7 @@ interface PromptProps { title: string } +// TODO: Re-use utils/Dialog export function useConfirm({ cancelTitle, title, onSubmit, submitTitle }: PromptProps) { const [key, setKey] = React.useState(0) const [opened, setOpened] = React.useState(false) diff --git a/catalog/app/components/Dialog/Prompt.tsx b/catalog/app/components/Dialog/Prompt.tsx index 999d331fa6d..dd87c916468 100644 --- a/catalog/app/components/Dialog/Prompt.tsx +++ b/catalog/app/components/Dialog/Prompt.tsx @@ -4,19 +4,23 @@ import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' interface DialogProps { + children: React.ReactNode initialValue?: string onCancel: () => void onSubmit: (value: string) => void open: boolean + placeholder?: string title: string validate: (value: string) => Error | undefined } function Dialog({ + children, initialValue, - open, onCancel, onSubmit, + open, + placeholder, title, validate, }: DialogProps) { @@ -26,6 +30,7 @@ function Dialog({ const handleChange = React.useCallback((event) => setValue(event.target.value), []) const handleSubmit = React.useCallback( (event) => { + event.stopPropagation() event.preventDefault() setSubmitted(true) if (!error) onSubmit(value) @@ -37,11 +42,13 @@ function Dialog({
{title} + {children} {!!error && !!submitted && ( @@ -69,11 +76,19 @@ function Dialog({ interface PromptProps { initialValue?: string onSubmit: (value: string) => void + placeholder?: string title: string validate: (value: string) => Error | undefined } -export function usePrompt({ initialValue, title, onSubmit, validate }: PromptProps) { +// TODO: Re-use utils/Dialog +export function usePrompt({ + onSubmit, + initialValue, + placeholder, + validate, + title, +}: PromptProps) { const [key, setKey] = React.useState(0) const [opened, setOpened] = React.useState(false) const open = React.useCallback(() => { @@ -89,20 +104,22 @@ export function usePrompt({ initialValue, title, onSubmit, validate }: PromptPro [close, onSubmit], ) const render = React.useCallback( - () => ( + (children?: React.ReactNode) => ( ), - [initialValue, key, close, handleSubmit, opened, title, validate], + [close, handleSubmit, initialValue, key, opened, placeholder, title, validate], ) return React.useMemo( () => ({ diff --git a/catalog/app/components/Experiments/Experiments.js b/catalog/app/components/Experiments/Experiments.js deleted file mode 100644 index 25a064b7cde..00000000000 --- a/catalog/app/components/Experiments/Experiments.js +++ /dev/null @@ -1,51 +0,0 @@ -import * as R from 'ramda' -import * as React from 'react' - -// map of experiment name to array of variants -const EXPERIMENTS = { - cta: [ - 'Ready to get your data organized?', - 'Ready to experiment faster?', - 'Ready to maximize return on data?', - ], - lede: ['Accelerate from data to impact', 'Manage data like code', 'Discover faster'], -} - -const Ctx = React.createContext() - -const pickRandom = (arr) => arr[Math.floor(Math.random() * arr.length)] - -const mapKeys = (fn) => - R.pipe( - R.toPairs, - R.map(([k, v]) => [fn(k, v), v]), - R.fromPairs, - ) - -export function ExperimentsProvider({ children }) { - const ref = React.useRef({}) - - const get = React.useCallback( - (name) => { - if (!(name in ref.current)) { - ref.current[name] = pickRandom(EXPERIMENTS[name]) - } - return ref.current[name] - }, - [ref], - ) - - const getSelectedVariants = React.useCallback( - (prefix = '') => mapKeys((k) => `${prefix}${k}`)(ref.current), - [ref], - ) - - return {children} -} - -export function useExperiments(experiment) { - const exps = React.useContext(Ctx) - return experiment ? exps.get(experiment) : exps -} - -export { ExperimentsProvider as Provider, useExperiments as use } diff --git a/catalog/app/components/Experiments/index.js b/catalog/app/components/Experiments/index.js deleted file mode 100644 index f0a23d3783a..00000000000 --- a/catalog/app/components/Experiments/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from './Experiments' diff --git a/catalog/app/components/Footer/Footer.js b/catalog/app/components/Footer/Footer.js index ab3cae20dd0..28850786fa1 100644 --- a/catalog/app/components/Footer/Footer.js +++ b/catalog/app/components/Footer/Footer.js @@ -1,4 +1,3 @@ -import cx from 'classnames' import * as React from 'react' import { Link } from 'react-router-dom' import * as M from '@material-ui/core' @@ -75,7 +74,6 @@ const NavIcon = ({ icon, ...props }) => ( ) const useStyles = M.makeStyles((t) => ({ - padded: {}, root: { background: `left / 64px url(${bg})`, boxShadow: [ @@ -91,14 +89,6 @@ const useStyles = M.makeStyles((t) => ({ display: 'flex', paddingTop: 0, }, - // padding for marketing CTA - '&$padded': { - [t.breakpoints.down('sm')]: { - backgroundSize: 'contain', - height: 230 + 64, - paddingBottom: 64, - }, - }, }, container: { alignItems: 'center', @@ -135,9 +125,7 @@ export default function Footer() { const reservedSpaceForIntercom = !intercom.dummy && !intercom.isCustom return ( -