From 029b5967393026360db2b17693c63ba9177fd08d Mon Sep 17 00:00:00 2001 From: Peter Szerzo Date: Fri, 25 Oct 2024 10:37:23 +0200 Subject: [PATCH] Handle uploads --- package-lock.json | 6 +-- packages/chat-core/src/index.ts | 26 ++++++++++ packages/chat-widget/src/icons.tsx | 10 +++- packages/chat-widget/src/index.tsx | 24 +++++++++- packages/chat-widget/src/ui/components.tsx | 56 ++++++++++++++++++++++ 5 files changed, 117 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c21bf873..0cd2583d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19206,9 +19206,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "bin": { "rollup": "dist/bin/rollup" diff --git a/packages/chat-core/src/index.ts b/packages/chat-core/src/index.ts index 6cff1555..bac45269 100644 --- a/packages/chat-core/src/index.ts +++ b/packages/chat-core/src/index.ts @@ -127,6 +127,10 @@ export interface BotResponseMetadata { * Whether the current conversation has been marked as incomprehension. */ incomprehension?: boolean; + /** + * Upload URL's + */ + uploadUrls: UploadUrl[]; /** * Whether the client should poll for more bot responses. */ @@ -176,6 +180,20 @@ export interface BotMessage { selectedChoiceId?: string; } +/** + * The upload destination for handling conversing with files + */ +export interface UploadUrl { + /** + * The URL of the upload + */ + url: string; + /** + * The ID of the upload + */ + uploadId: string; +} + /** * A choices to show to the user. */ @@ -401,6 +419,14 @@ export interface StructuredRequest { * The slots to populate */ slots?: SlotsRecordOrArray; + /** + * Upload ID + */ + uploadId?: string; + /** + * Upload utterance + */ + utterance?: string; /** * @hidden * This is used internally to indicate that the client is polling the bot for more data. diff --git a/packages/chat-widget/src/icons.tsx b/packages/chat-widget/src/icons.tsx index 3c62c79e..2877489c 100644 --- a/packages/chat-widget/src/icons.tsx +++ b/packages/chat-widget/src/icons.tsx @@ -27,11 +27,19 @@ export const ChatIcon = (): ReactNode => ( // eslint-disable-next-line jsdoc/require-returns /** @hidden @internal */ export const SendIcon = (): ReactNode => ( - + ); +// eslint-disable-next-line jsdoc/require-returns +/** @hidden @internal */ +export const AddPhotoIcon = (): ReactNode => ( + + + +); + // eslint-disable-next-line jsdoc/require-returns /** @hidden @internal */ export const DownloadIcon = (): ReactNode => ( diff --git a/packages/chat-widget/src/index.tsx b/packages/chat-widget/src/index.tsx index fcc03eb2..767405a3 100644 --- a/packages/chat-widget/src/index.tsx +++ b/packages/chat-widget/src/index.tsx @@ -18,7 +18,9 @@ import { ThemeProvider } from "@emotion/react"; import { useChat, type ChatHook } from "@nlxai/chat-react"; import { type Response, + type BotResponse, type ConversationHandler, + type UploadUrl, getCurrentExpirationTimestamp, } from "@nlxai/chat-core"; import { @@ -26,9 +28,10 @@ import { MinimizeIcon, ChatIcon, SendIcon, + AddPhotoIcon, ErrorOutlineIcon, } from "./icons"; -import { last, equals } from "ramda"; +import { last, equals, findLast } from "ramda"; import * as constants from "./ui/constants"; import { type Props, @@ -352,6 +355,17 @@ const isInputDisabled = (responses: Response[]): boolean => { return new URLSearchParams(payload).get("nlx:input-disabled") === "true"; }; +const findActiveUpload = (responses: Response[]): UploadUrl | null => { + const lastBotResponse = findLast( + (response): response is BotResponse => response.type === "bot", + responses, + ); + if (lastBotResponse == null) { + return null; + } + return lastBotResponse.payload.metadata?.uploadUrls?.[0] ?? null; +}; + /** * Hook to get the ConversationHandler for the widget. * This may be called before the Widget has been created. @@ -565,6 +579,8 @@ export const Widget = forwardRef(function Widget(props, ref) { [props.theme, windowInnerHeightValue, inputDisabled], ); + const activeUpload = findActiveUpload(chat.responses); + return ( @@ -652,6 +668,12 @@ export const Widget = forwardRef(function Widget(props, ref) { }} /> + {activeUpload == null ? null : ( + + + + + )} { diff --git a/packages/chat-widget/src/ui/components.tsx b/packages/chat-widget/src/ui/components.tsx index 3fdc9aa4..e905a67a 100644 --- a/packages/chat-widget/src/ui/components.tsx +++ b/packages/chat-widget/src/ui/components.tsx @@ -358,6 +358,59 @@ export const IconButton = styled.button<{ ${hoverBg} `; +export const UploadIconLabel = styled.label<{ + /** @hidden @internal */ + disabled?: boolean; +}>` + height: 35px; + width: 35px; + border-radius: 18px; + flex: none; + padding: 8px; + font-size: ${constants.fontSize}px; + ${(props) => + props.disabled === true + ? ` + opacity: 0.6; + ` + : ` + `} + border: 0; + box-shadow: none; + color: ${(props) => props.theme.primaryColor}; + background: none; + position: relative; + cursor: pointer; + + :focus { + outline: none; + ${(props) => focusShadow(props.theme)} + } + + :disabled { + cursor: auto; + } + + svg { + fill: ${(props) => props.theme.primaryColor}; + } + + input { + /** Screen-reader-only implementation */ + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } + + ${hoverBg} +`; + export const BottomButtonsContainer = styled.div<{ /** * @@ -368,6 +421,8 @@ export const BottomButtonsContainer = styled.div<{ top: 50%; right: ${(props) => `${props.theme.spacing}px`}; transform: translate3d(0, -50%, 0); + display: flex; + align-items: center; `; export const Input = styled.input<{ @@ -463,6 +518,7 @@ interface PinBubbleProps { */ onClick: () => void; } + // eslint-disable-next-line jsdoc/require-returns, jsdoc/require-param /** @hidden @internal */ export const PinBubble: React.FunctionComponent = (