diff --git a/.ort.yml b/.ort.yml index d32daf4d19..362bd5e586 100644 --- a/.ort.yml +++ b/.ort.yml @@ -31,7 +31,8 @@ resolutions: 'NPM:@plotly:mapbox-gl:1\\.13\\.4'\\." reason: 'CANT_FIX_EXCEPTION' comment: 'The dependency has BSD-3-Clause license in github repo' - - message: "The license NOASSERTION is currently not covered by policy rules\\. \ + - message: + "The license NOASSERTION is currently not covered by policy rules\\. \ The license was declared in package 'NPM:@plotly:mapbox-gl:1\\.13\\.4'\\." reason: 'CANT_FIX_EXCEPTION' comment: 'The dependency has BSD-3-Clause license in github repo' diff --git a/apps/chat/src/components/Chat/Chat.tsx b/apps/chat/src/components/Chat/Chat.tsx index 9c34ac4eb7..568d8570a1 100644 --- a/apps/chat/src/components/Chat/Chat.tsx +++ b/apps/chat/src/components/Chat/Chat.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; import { clearStateForMessages } from '@/src/utils/app/clear-messages-state'; +import { getConversationModelParams } from '@/src/utils/app/conversation'; import { isSmallScreen } from '@/src/utils/app/mobile'; import { @@ -14,7 +15,6 @@ import { LikeState, MergedMessages, Message, - Replay, Role, } from '@/src/types/chat'; import { EntityType, UploadStatus } from '@/src/types/common'; @@ -39,7 +39,6 @@ import { SettingsSelectors } from '@/src/store/settings/settings.reducers'; import { UISelectors } from '@/src/store/ui/ui.reducers'; import { REPLAY_AS_IS_MODEL } from '@/src/constants/chat'; -import { DEFAULT_ASSISTANT_SUBMODEL_ID } from '@/src/constants/default-ui-settings'; import Loader from '../Common/Loader'; import { NotFoundEntity } from '../Common/NotFoundEntity'; @@ -334,51 +333,6 @@ export const ChatView = memo(() => { [dispatch], ); - const applySelectedModel = useCallback( - ( - conversation: Conversation, - modelId: string | undefined, - ): Partial => { - if (modelId === REPLAY_AS_IS_MODEL && conversation.replay) { - return { - replay: { - ...conversation.replay, - replayAsIs: true, - }, - }; - } - const newAiEntity = modelId ? modelsMap[modelId] : undefined; - if (!modelId || !newAiEntity) { - return {}; - } - - const updatedReplay: Replay | undefined = !conversation.replay?.isReplay - ? conversation.replay - : { - ...conversation.replay, - replayAsIs: false, - }; - const updatedAddons = - conversation.replay && - conversation.replay.isReplay && - conversation.replay.replayAsIs && - !updatedReplay?.replayAsIs - ? conversation.selectedAddons.filter((addonId) => addonsMap[addonId]) - : conversation.selectedAddons; - - return { - model: { id: newAiEntity.reference }, - assistantModelId: - newAiEntity.type === EntityType.Assistant - ? DEFAULT_ASSISTANT_SUBMODEL_ID - : undefined, - replay: updatedReplay, - selectedAddons: updatedAddons, - }; - }, - [addonsMap, modelsMap], - ); - const handleSelectModel = useCallback( (conversation: Conversation, modelId: string) => { const newAiEntity = modelsMap[modelId]; @@ -390,12 +344,17 @@ export const ChatView = memo(() => { ConversationsActions.updateConversation({ id: conversation.id, values: { - ...applySelectedModel(conversation, modelId), + ...getConversationModelParams( + conversation, + modelId, + modelsMap, + addonsMap, + ), }, }), ); }, - [applySelectedModel, dispatch, modelsMap], + [addonsMap, dispatch, modelsMap], ); const handleSelectAssistantSubModel = useCallback( @@ -560,7 +519,12 @@ export const ChatView = memo(() => { id: conversation.id, values: { messages: clearStateForMessages(conversation.messages), - ...applySelectedModel(conversation, temporarySettings.modelId), + ...getConversationModelParams( + conversation, + temporarySettings.modelId, + modelsMap, + addonsMap, + ), prompt: temporarySettings.prompt, temperature: temporarySettings.temperature, assistantModelId: temporarySettings.currentAssistentModelId, @@ -573,7 +537,7 @@ export const ChatView = memo(() => { ); } }); - }, [selectedConversations, dispatch, applySelectedModel, addonsMap]); + }, [selectedConversations, dispatch, modelsMap, addonsMap]); const handleTemporarySettingsSave = useCallback( (conversation: Conversation, args: ConversationsTemporarySettings) => { diff --git a/apps/chat/src/components/Chat/ModelList.tsx b/apps/chat/src/components/Chat/ModelList.tsx index 270fc38283..f8c2e550bc 100644 --- a/apps/chat/src/components/Chat/ModelList.tsx +++ b/apps/chat/src/components/Chat/ModelList.tsx @@ -118,6 +118,11 @@ const ModelGroup = ({ }), ); + const handleSelectVersion = useCallback( + (entity: DialAIEntityModel) => onSelect(entity.id), + [onSelect], + ); + const menuItems: DisplayMenuItemProps[] = useMemo( () => [ { @@ -227,7 +232,7 @@ const ModelGroup = ({ {isCustomApplicationsEnabled && diff --git a/apps/chat/src/components/Chat/ModelVersionSelect.tsx b/apps/chat/src/components/Chat/ModelVersionSelect.tsx index 8776784942..7df47b14a4 100644 --- a/apps/chat/src/components/Chat/ModelVersionSelect.tsx +++ b/apps/chat/src/components/Chat/ModelVersionSelect.tsx @@ -1,8 +1,8 @@ -import { MouseEvent, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; import classNames from 'classnames'; -import { DialAIEntity } from '@/src/types/models'; +import { DialAIEntity, DialAIEntityModel } from '@/src/types/models'; import { Menu, MenuItem } from '@/src/components/Common/DropdownMenu'; @@ -12,9 +12,9 @@ import ChevronDownIcon from '@/public/images/icons/chevron-down.svg'; import orderBy from 'lodash-es/orderBy'; interface ModelVersionSelectProps { - entities: DialAIEntity[]; + entities: DialAIEntityModel[]; currentEntity: DialAIEntity; - onSelect: (id: string) => void; + onSelect: (id: DialAIEntityModel) => void; className?: string; } @@ -26,8 +26,8 @@ export const ModelVersionSelect = ({ }: ModelVersionSelectProps) => { const [isOpen, setIsOpen] = useState(false); - const onChangeHandler = (e: MouseEvent) => { - onSelect(e.currentTarget.value); + const onChangeHandler = (entity: DialAIEntityModel) => { + onSelect(entity); setIsOpen(false); }; @@ -46,6 +46,7 @@ export const ModelVersionSelect = ({ type="contextMenu" placement="bottom-end" onOpenChange={setIsOpen} + listClassName="z-[60]" data-qa="model-version-select" trigger={
} value={entity.id} - onClick={onChangeHandler} + onClick={() => onChangeHandler(entity)} data-model-versions data-qa="model-version-option" /> diff --git a/apps/chat/src/components/Chatbar/ModelIcon.tsx b/apps/chat/src/components/Chatbar/ModelIcon.tsx index b0eb677b20..0aff9282d1 100644 --- a/apps/chat/src/components/Chatbar/ModelIcon.tsx +++ b/apps/chat/src/components/Chatbar/ModelIcon.tsx @@ -18,6 +18,7 @@ interface Props { animate?: boolean; isCustomTooltip?: boolean; isInvalid?: boolean; + enableShrinking?: boolean; } const ModelIconTemplate = memo( @@ -27,6 +28,7 @@ const ModelIconTemplate = memo( animate, entityId, isInvalid, + enableShrinking, }: Omit) => { const fallbackUrl = entity?.type === EntityType.Addon @@ -40,6 +42,7 @@ const ModelIconTemplate = memo( 'relative inline-block shrink-0 leading-none', isInvalid ? 'text-secondary' : 'text-primary', animate && 'animate-bounce', + enableShrinking && 'shrink', )} style={{ height: `${size}px`, width: `${size}px` }} > diff --git a/apps/chat/src/components/Common/FullScreenImages.tsx b/apps/chat/src/components/Common/FullScreenImages.tsx new file mode 100644 index 0000000000..96a5514982 --- /dev/null +++ b/apps/chat/src/components/Common/FullScreenImages.tsx @@ -0,0 +1,103 @@ +import { IconChevronLeft, IconChevronRight, IconX } from '@tabler/icons-react'; +import { useState } from 'react'; + +import Image from 'next/image'; + +import classNames from 'classnames'; + +import { useMobileSwipe } from '@/src/hooks/useMobileSwipe'; + +import { ModalState } from '@/src/types/modal'; + +import Modal from './Modal'; + +interface Props { + images: string[]; + alt: string; + onClose: () => void; + defaultIdx?: number; +} + +const FullScreenImages = ({ images, alt, onClose, defaultIdx }: Props) => { + const [currentImage, setCurrentImage] = useState(defaultIdx ?? 0); + + const swipeHandlers = useMobileSwipe({ + onSwipedLeft: () => { + if (currentImage + 1 < images.length) { + setCurrentImage((idx) => idx + 1); + } + }, + onSwipedRight: () => { + if (currentImage !== 0) { + setCurrentImage((idx) => idx - 1); + } + }, + }); + + return ( + +
+
+
+ + {currentImage + 1} / {images.length} + +
+ +
+ +
+ +
+ {alt} +
+ +
+
+
+ ); +}; + +export default FullScreenImages; diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationContent.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationContent.tsx new file mode 100644 index 0000000000..87af6cc111 --- /dev/null +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationContent.tsx @@ -0,0 +1,280 @@ +import { useTranslation } from 'next-i18next'; + +import { DialAIEntityModel } from '@/src/types/models'; +import { Translation } from '@/src/types/translation'; + +interface Props { + entity: DialAIEntityModel; +} + +// const calculateTranslateX = ( +// previewImgsCount: number, +// activeSlide: number, +// scrollWidth?: number, +// clientWidth?: number, +// ) => { +// if (!clientWidth || !scrollWidth) return 'none'; + +// const maxSlideIndex = previewImgsCount - 1; +// const slideWidth = scrollWidth / previewImgsCount; +// const isLastSlide = activeSlide === maxSlideIndex; + +// const lastSlideTranslateX = scrollWidth - clientWidth; + +// const baseTranslateX = slideWidth * activeSlide; + +// const adjustment = isSmallScreen() ? 0 : slideWidth / 3; + +// const translateX = isLastSlide +// ? lastSlideTranslateX +// : Math.max(0, baseTranslateX - adjustment); + +// return `translateX(-${translateX}px)`; +// }; + +export const ApplicationDetailsContent = ({ entity }: Props) => { + const { t } = useTranslation(Translation.Marketplace); + + // const dispatch = useAppDispatch(); + + // const { data: session } = useSession(); + + // const sliderRef = useRef(null); + + // const [activeSlide, setActiveSlide] = useState(0); + // const [fullScreenSlide, setFullScreenSlide] = useState(); + // const [isRate, setIsRate] = useState(false); + + // const swipeHandlers = useMobileSwipe({ + // onSwipedLeft: () => { + // setActiveSlide((slide) => + // slide >= previewImgsCount - 1 ? previewImgsCount - 1 : slide + 1, + // ); + // }, + // onSwipedRight: () => { + // setActiveSlide((slide) => (slide === 0 ? 0 : slide - 1)); + // }, + // }); + + // const previewImgsCount = application.previewImages.length; + // const totalRating = Object.values(application.rating).reduce( + // (totalRating, rating) => totalRating + rating, + // 0, + // ); + // const ratingEntries = Object.entries(application.rating); + // const averageRating = round( + // ratingEntries.reduce( + // (acc, [rating, count]) => acc + Number(rating) * count, + // 0, + // ) / totalRating, + // 1, + // ); + + return ( +
+ {entity.description && ( +
+
+ {/*
+
+ {application.previewImages.map((image, idx) => ( +
+ setFullScreenSlide(idx)} + src={image} + alt={t('application preview')} + fill + className="cursor-pointer object-cover" + sizes="(max-width: 768px) 393px" + /> +
+ ))} +
+ + +
*/} +

{entity.description}

+
+
+ )} + {/*
+

{t('Capabilities')}

+
    + {application.capabilities.map((capability) => ( +
  • + +

    {capability}

    +
  • + ))} +
+
*/} + {/*
+
+

{t('Rating')}

+ +
+ {!isRate ? ( +
+
+

{averageRating}

+
+ {ratingEntries.map(([rating]) => ( +
+ Number(rating) && + 'text-accent-secondary [&_path]:fill-current', + )} + /> + {Math.ceil(averageRating) === Number(rating) && ( + + )} +
+ ))} +
+
+
    + {ratingEntries + .map(([rating, count]) => ( +
    + {rating} + +
    + )) + .reverse()} +
+
+ ) : ( +
+
+ {session?.user?.image ? ( + {t('User + ) : ( + + )} + John Dough +
+ + dispatch(UIActions.showSuccessToast(t('Rate sent'))) + } + onClose={() => setIsRate(false)} + /> +
+ )} +
*/} +
+
+

{t('Details')}

+ {/* */} +
+
+ {/*
+

{t('Author')}

+
+ {t('application + {application.author.name} +
+
*/} + {/*
+

{t('Release date')}

+ {entity.releaseDate} +
*/} +
+

{t('Version')}

+ + {entity.version ?? + t("This {{type}} don't versions", { + type: entity.type, + })} + +
+
+
+ {/* {fullScreenSlide !== undefined && ( + setFullScreenSlide(undefined)} + defaultIdx={fullScreenSlide} + /> + )} */} +
+ ); +}; diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationDetails.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationDetails.tsx new file mode 100644 index 0000000000..e807e7f672 --- /dev/null +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationDetails.tsx @@ -0,0 +1,121 @@ +import { useCallback, useMemo, useState } from 'react'; + +import { useSearchParams } from 'next/navigation'; +import { useRouter } from 'next/router'; + +import { compareIdWithQueryParamId } from '@/src/utils/app/common'; +import { getConversationModelParams } from '@/src/utils/app/conversation'; + +import { EntityType } from '@/src/types/common'; +import { ModalState } from '@/src/types/modal'; +import { DialAIEntityModel } from '@/src/types/models'; + +import { AddonsSelectors } from '@/src/store/addons/addons.reducers'; +import { + ConversationsActions, + ConversationsSelectors, +} from '@/src/store/conversations/conversations.reducers'; +import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; +import { ModelsSelectors } from '@/src/store/models/models.reducers'; + +import { DEFAULT_CONVERSATION_NAME } from '@/src/constants/default-ui-settings'; +import { MarketplaceQueryParams } from '@/src/constants/marketplace'; + +import Modal from '../../Common/Modal'; +import { ApplicationDetailsContent } from './ApplicationContent'; +import { ApplicationDetailsFooter } from './ApplicationFooter'; +import { ApplicationDetailsHeader } from './ApplicationHeader'; + +interface Props { + onClose: () => void; + entity: DialAIEntityModel; +} + +const ApplicationDetails = ({ onClose, entity }: Props) => { + const dispatch = useAppDispatch(); + + const router = useRouter(); + const searchParams = useSearchParams(); + + const [selectedVersionEntity, setSelectedVersionEntity] = useState(entity); + + const entities = useAppSelector(ModelsSelectors.selectModels); + const modelsMap = useAppSelector(ModelsSelectors.selectModelsMap); + const addonsMap = useAppSelector(AddonsSelectors.selectAddonsMap); + + const selectedConversations = useAppSelector( + ConversationsSelectors.selectSelectedConversations, + ); + + const filteredEntities = useMemo(() => { + return entities.filter((e) => entity.name === e.name); + }, [entities, entity.name]); + + const handleUseEntity = useCallback(() => { + const queryParamId = searchParams.get( + MarketplaceQueryParams.fromConversation, + ); + const conversationToApplyModel = selectedConversations.find((conv) => + compareIdWithQueryParamId(conv.id, queryParamId), + ); + + if (conversationToApplyModel) { + dispatch( + ConversationsActions.updateConversation({ + id: conversationToApplyModel.id, + values: { + ...getConversationModelParams( + conversationToApplyModel, + entity.reference, + modelsMap, + addonsMap, + ), + }, + }), + ); + } else { + dispatch( + ConversationsActions.createNewConversations({ + names: [DEFAULT_CONVERSATION_NAME], + modelReference: entity.reference, + }), + ); + } + + router.push('/'); + }, [ + addonsMap, + dispatch, + entity.reference, + modelsMap, + router, + searchParams, + selectedConversations, + ]); + + return ( + + + + + + ); +}; + +export default ApplicationDetails; diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationFooter.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationFooter.tsx new file mode 100644 index 0000000000..f3b8a65b7b --- /dev/null +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationFooter.tsx @@ -0,0 +1,56 @@ +import { IconPlayerPlay, IconShare } from '@tabler/icons-react'; + +import { useTranslation } from 'next-i18next'; + +import { DialAIEntityModel } from '@/src/types/models'; +import { Translation } from '@/src/types/translation'; + +import { ModelVersionSelect } from '../../Chat/ModelVersionSelect'; + +interface Props { + modelType: string; + entity: DialAIEntityModel; + entities: DialAIEntityModel[]; + onChangeVersion: (entity: DialAIEntityModel) => void; + onUseEntity: () => void; +} + +export const ApplicationDetailsFooter = ({ + modelType, + entities, + entity, + onChangeVersion, + onUseEntity, +}: Props) => { + const { t } = useTranslation(Translation.Marketplace); + + return ( +
+
+ +
+ + +
+
+
+ ); +}; diff --git a/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx new file mode 100644 index 0000000000..ab81496396 --- /dev/null +++ b/apps/chat/src/components/Marketplace/ApplicationDetails/ApplicationHeader.tsx @@ -0,0 +1,148 @@ +import { + IconBrandFacebook, + IconBrandX, + IconLink, + IconShare, + IconX, +} from '@tabler/icons-react'; +import { useMemo } from 'react'; + +import { useTranslation } from 'next-i18next'; + +import classNames from 'classnames'; + +import { isSmallScreen } from '@/src/utils/app/mobile'; +import { translate } from '@/src/utils/app/translation'; + +import { DialAIEntityModel } from '@/src/types/models'; +import { Translation } from '@/src/types/translation'; + +import { useAppDispatch } from '@/src/store/hooks'; +import { UIActions } from '@/src/store/ui/ui.reducers'; + +import { ModelIcon } from '../../Chatbar/ModelIcon'; +import { Menu, MenuItem } from '../../Common/DropdownMenu'; + +interface Props { + entity: DialAIEntityModel; + onClose: () => void; +} + +export const ApplicationDetailsHeader = ({ entity, onClose }: Props) => { + const { t } = useTranslation(Translation.Marketplace); + + const dispatch = useAppDispatch(); + + const contextMenuItems = useMemo( + () => [ + { + BrandIcon: IconLink, + text: translate('Copy link'), + onClick: () => { + dispatch(UIActions.showInfoToast(t('Link copied'))); + }, + }, + { + BrandIcon: IconBrandFacebook, + text: translate('Share via Facebook'), + onClick: () => { + const shareUrl = encodeURIComponent(window.location.href); // link to app + window.open(`https://m.me/?link=${shareUrl}`, '_blank'); + }, + }, + { + BrandIcon: IconBrandX, + text: translate('Share via X'), + onClick: () => { + const shareUrl = encodeURIComponent(window.location.href); // link to app + window.open( + `https://twitter.com/messages/compose?text=${shareUrl}`, + '_blank', + ); + }, + }, + ], + [dispatch, t], + ); + + return ( +
+
+ +
+
+
+
+ {/* {application.tags.map((tag) => ( + + ))} */} +

+ {entity.name} +

+
+
+ + + {t('Share')} + + } + > +
+
+ +
{entity.name}
+
+
+ {contextMenuItems.map(({ BrandIcon, text, ...props }) => ( + + + {text} + + } + className="flex w-full items-center gap-3 px-3 py-2 hover:bg-accent-primary-alpha" + {...props} + /> + ))} +
+
+
+ +
+
+ {/*

+ {application.title} +

*/} +
+
+ ); +}; diff --git a/apps/chat/src/components/Marketplace/ApplicationTag.tsx b/apps/chat/src/components/Marketplace/ApplicationTag.tsx new file mode 100644 index 0000000000..b130531446 --- /dev/null +++ b/apps/chat/src/components/Marketplace/ApplicationTag.tsx @@ -0,0 +1,28 @@ +import classNames from 'classnames'; + +interface Props { + tag: string; +} + +enum TagTypes { + Analysis = 'Analysis', + SQL = 'SQL', + Development = 'Development', +} + +export const ApplicationTag = ({ tag }: Props) => { + return ( + + {tag} + + ); +}; diff --git a/apps/chat/src/components/Marketplace/Marketplace.tsx b/apps/chat/src/components/Marketplace/Marketplace.tsx index 9ed9bb03f0..7fd4d7a779 100644 --- a/apps/chat/src/components/Marketplace/Marketplace.tsx +++ b/apps/chat/src/components/Marketplace/Marketplace.tsx @@ -1,4 +1,6 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; + +import { DialAIEntityModel } from '@/src/types/models'; import { useAppDispatch, useAppSelector } from '@/src/store/hooks'; import { @@ -8,6 +10,8 @@ import { import { Spinner } from '@/src/components/Common/Spinner'; +import ApplicationDetails from './ApplicationDetails/ApplicationDetails'; + const Marketplace = () => { const dispatch = useAppDispatch(); @@ -15,6 +19,8 @@ const Marketplace = () => { const isModelsLoaded = useAppSelector(ModelsSelectors.selectIsModelsLoaded); const models = useAppSelector(ModelsSelectors.selectModels); + const [detailsModel, setDetailsModel] = useState(); + useEffect(() => { if (!isModelsLoaded && !isModelsLoading) { dispatch(ModelsActions.getModels()); @@ -30,13 +36,20 @@ const Marketplace = () => { {models.map((model) => (
setDetailsModel(model)} + className="h-[92px] cursor-pointer rounded border border-primary bg-transparent p-4 md:h-[203px] xl:h-[207px]" > {model.name}
))}
)} + {detailsModel && ( + setDetailsModel(undefined)} + /> + )} ); }; diff --git a/apps/chat/src/components/Marketplace/Rating/RatingHandler.tsx b/apps/chat/src/components/Marketplace/Rating/RatingHandler.tsx new file mode 100644 index 0000000000..bcdb826a94 --- /dev/null +++ b/apps/chat/src/components/Marketplace/Rating/RatingHandler.tsx @@ -0,0 +1,69 @@ +import { IconStarFilled } from '@tabler/icons-react'; +import { useState } from 'react'; + +import { useTranslation } from 'next-i18next'; + +import classNames from 'classnames'; + +import { Translation } from '@/src/types/translation'; + +const starIdxs = [1, 2, 3, 4, 5]; + +interface Props { + onRatingApply: () => void; + onClose: () => void; +} + +export const RatingHandler = ({ onRatingApply, onClose }: Props) => { + const { t } = useTranslation(Translation.Marketplace); + + const [selectedRating, setSelectedRating] = useState(0); + const [hoveredStars, setHoveredStars] = useState(0); + + return ( + <> +
setHoveredStars(0)}> + {starIdxs.map((rating) => ( +
= rating || selectedRating >= rating) && + '[&_path]:fill-transparent', + )} + key={rating} + > + = rating || selectedRating >= rating) && + 'text-accent-secondary [&_path]:fill-current', + )} + /> + setSelectedRating(rating)} + onMouseEnter={() => setHoveredStars(rating)} + className="absolute top-0 size-full shrink-0 cursor-pointer appearance-none border-none" + type="radio" + name="rate" + /> +
+ ))} +
+
+ + +
+ + ); +}; diff --git a/apps/chat/src/components/Marketplace/Rating/RatingProgressBar.tsx b/apps/chat/src/components/Marketplace/Rating/RatingProgressBar.tsx new file mode 100644 index 0000000000..6a2ca001f6 --- /dev/null +++ b/apps/chat/src/components/Marketplace/Rating/RatingProgressBar.tsx @@ -0,0 +1,15 @@ +interface Props { + total: number; + count: number; +} + +export const RatingProgressBar = ({ total, count }: Props) => { + return ( +
+
+
+ ); +}; diff --git a/apps/chat/src/constants/marketplace.ts b/apps/chat/src/constants/marketplace.ts new file mode 100644 index 0000000000..36d86aebad --- /dev/null +++ b/apps/chat/src/constants/marketplace.ts @@ -0,0 +1,3 @@ +export enum MarketplaceQueryParams { + fromConversation = 'fromConversation', +} diff --git a/apps/chat/src/hooks/useMobileSwipe.ts b/apps/chat/src/hooks/useMobileSwipe.ts new file mode 100644 index 0000000000..6cf69b2a99 --- /dev/null +++ b/apps/chat/src/hooks/useMobileSwipe.ts @@ -0,0 +1,47 @@ +import { useState } from 'react'; + +type TouchEvent = React.TouchEvent; + +export const useMobileSwipe = ({ + onSwipedLeft, + onSwipedRight, +}: { + onSwipedLeft: () => void; + onSwipedRight: () => void; +}) => { + const [touchStart, setTouchStart] = useState(); + const [touchEnd, setTouchEnd] = useState(); + + const onTouchStart = (e: TouchEvent) => { + setTouchStart(e.targetTouches[0].clientX); + setTouchEnd(undefined); + }; + + const onTouchMove = (e: TouchEvent) => { + setTouchEnd(e.targetTouches[0].clientX); + }; + + const onTouchEnd = () => { + if (!touchStart || !touchEnd) return; + + const distance = touchStart - touchEnd; + const minDistance = 50; + + if (distance > minDistance) { + onSwipedLeft(); + } + + if (distance < -minDistance) { + onSwipedRight(); + } + + setTouchStart(undefined); + setTouchEnd(undefined); + }; + + return { + onTouchStart, + onTouchMove, + onTouchEnd, + }; +}; diff --git a/apps/chat/src/store/conversations/conversations.epics.ts b/apps/chat/src/store/conversations/conversations.epics.ts index 9fd1909fe5..d9c0549bc2 100644 --- a/apps/chat/src/store/conversations/conversations.epics.ts +++ b/apps/chat/src/store/conversations/conversations.epics.ts @@ -362,6 +362,7 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => conversations: ConversationsSelectors.selectConversations(state$.value), shouldUploadConversationsForCompare: payload.shouldUploadConversationsForCompare, + modelReference: payload.modelReference, })), switchMap( ({ @@ -369,8 +370,10 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => lastConversation, conversations, shouldUploadConversationsForCompare, + modelReference, }) => forkJoin({ + modelReference: of(modelReference), names: of(names), lastConversation: lastConversation && lastConversation.status !== UploadStatus.LOADED @@ -397,7 +400,7 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => : of(conversations), }), ), - switchMap(({ names, lastConversation, conversations }) => { + switchMap(({ names, lastConversation, conversations, modelReference }) => { return state$.pipe( startWith(state$.value), map((state) => { @@ -406,8 +409,14 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => SettingsSelectors.selectIsolatedModelId(state); if (isIsolatedView && isolatedModelId) { const models = ModelsSelectors.selectModels(state); - return models.filter((i) => i?.reference === isolatedModelId); + return models.filter((i) => i?.reference === isolatedModelId)[0] + ?.reference; } + + if (modelReference) { + return modelReference; + } + const recentModels = ModelsSelectors.selectRecentModels(state); if (lastConversation?.model.id) { const lastModelId = lastConversation.model.id; @@ -415,18 +424,18 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => return [ ...models.filter((i) => i?.reference === lastModelId), ...recentModels, - ]; + ][0]?.reference; } - return recentModels; + + return recentModels[0]?.reference; }), - filter((models) => models && models.length > 0), + filter(Boolean), take(1), - switchMap((recentModels) => { - const model = recentModels[0]; - - if (!model) { + switchMap((modelReference) => { + if (!modelReference) { return EMPTY; } + const conversationRootId = getConversationRootId(); const newConversations: Conversation[] = names.map( (name, index): Conversation => @@ -443,7 +452,7 @@ const createNewConversationsEpic: AppEpic = (action$, state$) => ), messages: [], model: { - id: model.reference, + id: modelReference, }, prompt: DEFAULT_SYSTEM_PROMPT, temperature: diff --git a/apps/chat/src/store/conversations/conversations.reducers.ts b/apps/chat/src/store/conversations/conversations.reducers.ts index a5948c393d..6b872c7ba3 100644 --- a/apps/chat/src/store/conversations/conversations.reducers.ts +++ b/apps/chat/src/store/conversations/conversations.reducers.ts @@ -191,6 +191,7 @@ export const conversationsSlice = createSlice({ state, _action: PayloadAction<{ names: string[]; + modelReference?: string; shouldUploadConversationsForCompare?: boolean; suspendHideSidebar?: boolean; }>, diff --git a/apps/chat/src/utils/app/common.ts b/apps/chat/src/utils/app/common.ts index 312b584057..5aa5e22df2 100644 --- a/apps/chat/src/utils/app/common.ts +++ b/apps/chat/src/utils/app/common.ts @@ -10,7 +10,7 @@ import { EntityFilters } from '@/src/types/search'; import { MAX_ENTITY_LENGTH } from '@/src/constants/default-ui-settings'; import { NA_VERSION } from '@/src/constants/public'; -import { getPublicItemIdWithoutVersion } from '../server/api'; +import { ApiUtils, getPublicItemIdWithoutVersion } from '../server/api'; import { doesEntityContainSearchTerm } from './search'; import groupBy from 'lodash-es/groupBy'; @@ -253,3 +253,8 @@ export const groupAllVersions = ( ? [{ version: latestVersion, id: latestVersionItemId }] : []; }); + +export const compareIdWithQueryParamId = ( + id: string, + queryParamId: string | null, +) => ApiUtils.encodeApiUrl(id) === queryParamId; diff --git a/apps/chat/src/utils/app/conversation.ts b/apps/chat/src/utils/app/conversation.ts index 0524435488..e4828fb09a 100644 --- a/apps/chat/src/utils/app/conversation.ts +++ b/apps/chat/src/utils/app/conversation.ts @@ -8,6 +8,7 @@ import { ConversationInfo, Message, MessageSettings, + Replay, Role, } from '@/src/types/chat'; import { EntityType, PartialBy, UploadStatus } from '@/src/types/common'; @@ -17,6 +18,9 @@ import { DialAIEntityModel, } from '@/src/types/models'; +import { REPLAY_AS_IS_MODEL } from '@/src/constants/chat'; +import { DEFAULT_ASSISTANT_SUBMODEL_ID } from '@/src/constants/default-ui-settings'; + import { getConversationApiKey, parseConversationApiKey } from '../server/api'; import { constructPath } from './file'; import { splitEntityId } from './folders'; @@ -287,3 +291,47 @@ export const addPausedError = ( return message; }); }; + +export const getConversationModelParams = ( + conversation: Conversation, + modelId: string | undefined, + modelsMap: Partial>, + addonsMap: Partial>, +): Partial => { + if (modelId === REPLAY_AS_IS_MODEL && conversation.replay) { + return { + replay: { + ...conversation.replay, + replayAsIs: true, + }, + }; + } + const newAiEntity = modelId ? modelsMap[modelId] : undefined; + if (!modelId || !newAiEntity) { + return {}; + } + + const updatedReplay: Replay | undefined = !conversation.replay?.isReplay + ? conversation.replay + : { + ...conversation.replay, + replayAsIs: false, + }; + const updatedAddons = + conversation.replay && + conversation.replay.isReplay && + conversation.replay.replayAsIs && + !updatedReplay?.replayAsIs + ? conversation.selectedAddons.filter((addonId) => addonsMap[addonId]) + : conversation.selectedAddons; + + return { + model: { id: newAiEntity.reference }, + assistantModelId: + newAiEntity.type === EntityType.Assistant + ? DEFAULT_ASSISTANT_SUBMODEL_ID + : undefined, + replay: updatedReplay, + selectedAddons: updatedAddons, + }; +};