apps/chat/src/components/Chat/ChatSettings/Addons.tsx (226 lines of code) (raw):

import { IconX } from '@tabler/icons-react'; import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'next-i18next'; import classNames from 'classnames'; import { DialAIEntityAddon } from '@/src/types/models'; import { Translation } from '@/src/types/translation'; import { AddonsSelectors } from '@/src/store/addons/addons.reducers'; import { useAppSelector } from '@/src/store/hooks'; import { ModelIcon } from '../../Chatbar/ModelIcon'; import { EntityMarkdownDescription } from '../../Common/MarkdownDescription'; import Tooltip from '../../Common/Tooltip'; import { AddonsDialog } from './AddonsDialog'; interface AddonProps { addonId: string; isSelected: boolean; preselectedAddonsIds: string[]; disabled: boolean; onChangeAddon: (addonId: string) => void; } const AddonButton = ({ addonId, preselectedAddonsIds, isSelected, disabled, onChangeAddon, }: AddonProps) => { const addonsMap = useAppSelector(AddonsSelectors.selectAddonsMap); return ( <button className={classNames( 'flex items-center gap-2 rounded px-3 py-2 text-left disabled:cursor-not-allowed', { 'bg-accent-primary-alpha': isSelected }, { 'bg-layer-3 hover:bg-layer-4': !isSelected, }, )} disabled={disabled || preselectedAddonsIds.includes(addonId)} onClick={() => { onChangeAddon(addonId); }} > <ModelIcon entity={addonsMap[addonId]} entityId={addonId} size={15} /> <span>{addonsMap[addonId]?.name || addonId}</span> {isSelected && !preselectedAddonsIds.includes(addonId) && ( <IconX size={14} className="text-secondary" /> )} </button> ); }; const Addon = ({ addonId, preselectedAddonsIds, isSelected, disabled, onChangeAddon, }: AddonProps) => { const addonsMap = useAppSelector(AddonsSelectors.selectAddonsMap); const description = useMemo( () => addonsMap[addonId]?.description, [addonId, addonsMap], ); if (description) { return ( <Tooltip tooltip={ <EntityMarkdownDescription>{description}</EntityMarkdownDescription> } triggerClassName="flex shrink-0" contentClassName="max-w-[220px]" placement="top" > <AddonButton addonId={addonId} preselectedAddonsIds={preselectedAddonsIds} isSelected={isSelected} onChangeAddon={onChangeAddon} disabled={disabled} /> </Tooltip> ); } return ( <AddonButton addonId={addonId} preselectedAddonsIds={preselectedAddonsIds} isSelected={isSelected} onChangeAddon={onChangeAddon} disabled={disabled} /> ); }; interface AddonsProps { preselectedAddonsIds: string[]; selectedAddonsIds: string[]; disabled: boolean; onApplyAddons: (addonsIds: string[]) => void; onChangeAddon: (addonId: string) => void; } const filterRecentAddons = ( recentAddonsIds: string[], selectedAddonsIds: string[], preselectedAddonsIds: string[], addonsMap: Partial<Record<string, DialAIEntityAddon>>, ) => { return recentAddonsIds.filter( (id) => addonsMap[id] && !selectedAddonsIds.includes(id) && !preselectedAddonsIds.includes(id), ); }; export const Addons = ({ preselectedAddonsIds, selectedAddonsIds, disabled, onChangeAddon, onApplyAddons, }: AddonsProps) => { const { t } = useTranslation(Translation.Chat); const recentAddonsIds = useAppSelector(AddonsSelectors.selectRecentAddonsIds); const addonsMap = useAppSelector(AddonsSelectors.selectAddonsMap); const addons = useAppSelector(AddonsSelectors.selectAddons); const [filteredRecentAddons, setFilteredRecentAddons] = useState<string[]>( filterRecentAddons( recentAddonsIds, selectedAddonsIds, preselectedAddonsIds, addonsMap, ), ); const [isAddonsDialogOpen, setIsAddonsDialogOpen] = useState(false); useEffect(() => { setFilteredRecentAddons( filterRecentAddons( recentAddonsIds, selectedAddonsIds, preselectedAddonsIds, addonsMap, ), ); }, [selectedAddonsIds, preselectedAddonsIds, recentAddonsIds, addonsMap]); const handleCloseAddonsDialog = useCallback(() => { setIsAddonsDialogOpen(false); }, []); if (!addons.length) { return null; } return ( <div className="flex flex-col gap-3" data-qa="addons"> <span>{t('Addons (max 10)')}</span> {(selectedAddonsIds?.filter((id) => addonsMap[id]).length > 0 || preselectedAddonsIds?.length > 0) && ( <> <span className="text-secondary">{t('Selected')}</span> <div className="flex flex-wrap gap-1" data-qa="selected-addons"> {preselectedAddonsIds.map((addon) => ( <Addon key={addon} addonId={addon} isSelected onChangeAddon={onChangeAddon} preselectedAddonsIds={preselectedAddonsIds} disabled={disabled} /> ))} {selectedAddonsIds .filter( (id) => addonsMap[id] && !preselectedAddonsIds.includes(id), ) .map((addon) => ( <Addon key={addon} addonId={addon} isSelected onChangeAddon={onChangeAddon} preselectedAddonsIds={preselectedAddonsIds} disabled={disabled} /> ))} </div> </> )} {(!selectedAddonsIds || selectedAddonsIds.length + preselectedAddonsIds.length < 11) && ( <> {filteredRecentAddons?.length > 0 && ( <> <span className="text-secondary">{t('Recent')}</span> <div className="flex flex-wrap gap-1" data-qa="recent-addons"> {filteredRecentAddons .map((addon) => ( <Addon key={addon} addonId={addon} isSelected={false} onChangeAddon={onChangeAddon} preselectedAddonsIds={preselectedAddonsIds} disabled={disabled} /> )) .filter(Boolean)} </div> </> )} <div> <button disabled={disabled} className="mt-3 inline text-left text-accent-primary disabled:cursor-not-allowed" onClick={() => { setIsAddonsDialogOpen(true); }} data-qa="see-all-addons" > {t('See all addons')} </button> </div> <AddonsDialog isOpen={isAddonsDialogOpen} selectedAddonsIds={selectedAddonsIds} preselectedAddonsIds={preselectedAddonsIds} onClose={handleCloseAddonsDialog} onAddonsSelected={onApplyAddons} /> </> )} </div> ); };