diff --git a/package-lock.json b/package-lock.json index 72ae5bc..97bf6fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "reachat", - "version": "1.4.0", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "reachat", - "version": "1.4.0", + "version": "1.4.2", "license": "Apache-2.0", "dependencies": { "@radix-ui/react-slot": "^1.1.0", diff --git a/src/SessionMessages/SessionMessage/MessageResponseRecommended.tsx b/src/SessionMessages/SessionMessage/MessageResponseRecommended.tsx new file mode 100644 index 0000000..521b6e3 --- /dev/null +++ b/src/SessionMessages/SessionMessage/MessageResponseRecommended.tsx @@ -0,0 +1,82 @@ +import { ChatContext } from '@/ChatContext'; +import { Slot } from '@radix-ui/react-slot'; +import { motion } from 'framer-motion'; +import { cn } from 'reablocks'; +import { FC, PropsWithChildren, useContext } from 'react'; +import { Markdown } from '@/Markdown'; +import { PluggableList } from 'react-markdown/lib'; + +export interface MessageResponseRecommendedProps extends PropsWithChildren { + /** + * Follow-up response to render (array of values). + */ + followUpResponse: string[]; + + /** + * Whether the response is loading. + */ + isLoading?: boolean; + + /** + * Function to handle clicks on the follow-up response. + */ + onClickFollowUpResponse?: (followUpResponse: string) => void; +} + +// Helper function to check if the URL is a valid image URL +const isImageUrl = (url: string) => { + return /\.(jpeg|jpg|gif|png|svg|webp)$/i.test(url); +}; + +export const MessageResponseRecommended: FC< + MessageResponseRecommendedProps +> = ({ followUpResponse, isLoading, children, onClickFollowUpResponse }) => { + const { theme, isCompact, remarkPlugins } = useContext(ChatContext); + const Comp = children ? Slot : 'div'; + + return ( + + {children || ( + <> + {followUpResponse.map((responseItem, index) => ( +
onClickFollowUpResponse?.(responseItem)} + > + {isImageUrl(responseItem) ? ( +
+ {responseItem} +
+ ) : ( +
+ + {responseItem} + +
+ )} +
+ ))} + + {isLoading && ( + + )} + + )} +
+ ); +}; diff --git a/src/SessionMessages/SessionMessage/SessionMessage.tsx b/src/SessionMessages/SessionMessage/SessionMessage.tsx index 60372bf..3a975d4 100644 --- a/src/SessionMessages/SessionMessage/SessionMessage.tsx +++ b/src/SessionMessages/SessionMessage/SessionMessage.tsx @@ -7,6 +7,7 @@ import { MessageQuestion } from './MessageQuestion'; import { MessageResponse } from './MessageResponse'; import { MessageSources } from './MessageSources'; import { MessageActions } from './MessageActions'; +import { MessageResponseRecommended } from './MessageResponseRecommended'; const messageVariants = { hidden: { @@ -41,17 +42,32 @@ export const SessionMessage: FC = ({ children }) => { const { theme, isLoading } = useContext(ChatContext); + const hasFollowUpResponse = + Array.isArray(conversation.followUpResponse) && + conversation.followUpResponse.length > 0; return ( {children || ( <> - + + + + {hasFollowUpResponse && ( + + )} + = ({ )} - {!isLast && ( - - )} + {!isLast && } ); }; diff --git a/src/SessionMessages/SessionMessage/index.ts b/src/SessionMessages/SessionMessage/index.ts index 5eb14b0..0340978 100644 --- a/src/SessionMessages/SessionMessage/index.ts +++ b/src/SessionMessages/SessionMessage/index.ts @@ -6,3 +6,4 @@ export * from './MessageFiles'; export * from './MessageQuestion'; export * from './MessageResponse'; export * from './MessageSources'; +export * from './MessageResponseRecommended'; diff --git a/src/theme.ts b/src/theme.ts index 3c8e247..d35634c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -33,6 +33,9 @@ export interface ChatTheme { cursor: string; overlay: string; expand: string; + recommended: string; + rimage: string; + markdownBorder: string; files: { base: string; file: { @@ -132,10 +135,20 @@ export const chatTheme: ChatTheme = { 'relative font-semibold mb-4 px-4 py-4 pb-2 rounded-3xl rounded-br-none text-typography border bg-gray-200 border-gray-300 text-gray-900', 'dark:bg-gray-900/60 dark:border-gray-700/50 dark:text-gray-100' ].join(' '), - response: ['relative data-[compact=false]:px-4 text-gray-900', 'dark:text-gray-100'].join(' '), - overlay: `overflow-y-hidden max-h-[350px] after:content-[''] after:absolute after:inset-x-0 after:bottom-0 after:h-16 after:bg-gradient-to-b after:from-transparent dark:after:to-gray-900 after:to-gray-200`, + response: [ + 'relative data-[compact=false]:px-4 text-gray-900', + 'dark:text-gray-100' + ].join(' '), + overlay: + "overflow-y-hidden max-h-[350px] after:content-[''] after:absolute after:inset-x-0 after:bottom-0 after:h-16 after:bg-gradient-to-b after:from-transparent dark:after:to-gray-900 after:to-gray-200", cursor: 'inline-block w-1 h-4 bg-current', expand: 'absolute bottom-1 right-1 z-10', + recommended: 'flex gap-4 justify-around mt-8', + rimage: ' max-w-[250px] max-h-[250px] object-cover cursor-pointer', + markdownBorder: [ + 'relative font-semibold mb-4 px-4 py-4 pb-2 rounded-4xl rounded-[10px] cursor-pointer text-typography border bg-gray-200 border-gray-300 text-gray-900', + 'dark:bg-gray-900/60 dark:border-gray-700/50 dark:text-gray-100 ' + ].join(' '), files: { base: 'mb-2 flex flex-wrap gap-3 ', file: { @@ -167,7 +180,8 @@ export const chatTheme: ChatTheme = { th: 'px-4 py-2 text-left font-bold border-b border-gray-500', td: 'px-4 py-2', code: 'm-2 rounded-b relative', - toolbar: 'text-xs dark:bg-gray-700/50 flex items-center justify-between px-2 py-1 rounded-t sticky top-0 backdrop-blur-md bg-gray-200 ', + toolbar: + 'text-xs dark:bg-gray-700/50 flex items-center justify-between px-2 py-1 rounded-t sticky top-0 backdrop-blur-md bg-gray-200 ', li: 'mb-2 ml-6', ul: 'mb-4 list-disc', ol: 'mb-4 list-decimal' diff --git a/src/types.ts b/src/types.ts index 67cfe2f..0379dbe 100644 --- a/src/types.ts +++ b/src/types.ts @@ -62,6 +62,11 @@ export interface Conversation { * The AI's response to the user's question */ response?: string; + /** + * The AI's follow-upresponse to the user's question + */ + + followUpResponse?: string[]; /** * Array of sources referenced in the conversation diff --git a/stories/Console.stories.tsx b/stories/Console.stories.tsx index 785ed95..3743b71 100644 --- a/stories/Console.stories.tsx +++ b/stories/Console.stories.tsx @@ -43,7 +43,8 @@ import { fakeSessionsWithEmbeds, sessionWithSources, sessionsWithFiles, - sessionsWithPartialConversation + sessionsWithPartialConversation, + sessionWithMessageResponseRecommended } from './examples'; export default { @@ -1093,3 +1094,38 @@ export const ImageFiles = () => { ); }; + +export const FollowUpResponses = () => { + return ( +
+ + + + + + + + + + + + +
+ ); +}; diff --git a/stories/examples.ts b/stories/examples.ts index 2bb49b4..dd6e61a 100644 --- a/stories/examples.ts +++ b/stories/examples.ts @@ -176,3 +176,46 @@ export const sessionsWithPartialConversation: Session[] = [ ] } ]; + +export const sessionWithMessageResponseRecommended: Session[] = [ + { + id: 'session-1', + title: 'Session with Image', + createdAt: subHours(new Date(), 1), + updatedAt: new Date(), + conversations: [ + { + id: 'conversation-1', + question: 'What are the benefits of using React?', + createdAt: new Date(), + response: + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1024px-React-icon.svg.png', + followUpResponse: [ + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1024px-React-icon.svg.png', + 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/1024px-React-icon.svg.png' + ], + updatedAt: new Date() + } + ] + }, + { + id: 'session-2', + title: 'Session with Text', + createdAt: subHours(new Date(), 1), + updatedAt: new Date(), + conversations: [ + { + id: 'conversation-2', + question: 'What are the benefits of using React?', + createdAt: new Date(), + response: + 'React benefits include a declarative coding style, component-based architecture, virtual DOM, and a large community and ecosystem.', + followUpResponse: [ + 'What are some downsides of React?', + 'What are alternative options to React?' + ], + updatedAt: new Date() + } + ] + } +];