import * as React from 'react'; import { useShallow } from 'zustand/react/shallow'; import type { SxProps } from '@mui/joy/styles/types'; import { Alert, Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy'; import ClearIcon from '@mui/icons-material/Clear'; import DoneIcon from '@mui/icons-material/Done'; import EditRoundedIcon from '@mui/icons-material/EditRounded'; import EditNoteIcon from '@mui/icons-material/EditNote'; import SearchIcon from '@mui/icons-material/Search'; import TelegramIcon from '@mui/icons-material/Telegram'; import { SystemPurposeData, SystemPurposeExample, SystemPurposeId, SystemPurposes } from '../../../../data'; import { YouTubeURLInput } from '~/modules/youtube/YouTubeURLInput'; import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix'; import type { DConversationId } from '~/common/stores/chat/chat.conversation'; import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox'; import { createDMessageTextContent } from '~/common/stores/chat/chat.message'; import { lineHeightTextareaMd } from '~/common/app.theme'; import { navigateToPersonas } from '~/common/app.routes'; import { useChatStore } from '~/common/stores/chat/store-chats'; import { useChipBoolean } from '~/common/components/useChipBoolean'; import { useModelDomain } from '~/common/stores/llms/hooks/useModelDomain'; import { useUIPreferencesStore } from '~/common/stores/store-ui'; import { usePurposeStore } from './store-purposes'; // 'special' purpose IDs, for tile hiding purposes const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__'; const TILE_ACTIVE_COLOR = 'primary' as const; // defined looks const tileSize = 7; // rem const tileGap = 0.5; // rem function Tile(props: { text?: string, imageUrl?: string, symbol?: string, isActive: boolean, isEditMode: boolean, isHidden?: boolean, isHighlighted?: boolean, onClick: () => void, sx?: SxProps, }) { return ( ); } /** * Purpose selector for the current chat. Clicking on any item activates it for the current chat. */ export function PersonaSelector(props: { conversationId: DConversationId, isMobile: boolean, runExample: (example: SystemPurposeExample) => void, }) { // state const [searchQuery, setSearchQuery] = React.useState(''); const [filteredIDs, setFilteredIDs] = React.useState(null); const [editMode, setEditMode] = React.useState(false); // external state const { complexityMode, showPersonaFinder } = useUIPreferencesStore(useShallow(state => ({ complexityMode: state.complexityMode, showPersonaFinder: state.showPersonaFinder, }))); const [showExamples, showExamplescomponent] = useChipBoolean('Examples', complexityMode === 'extra' && !props.isMobile); const [showPrompt, showPromptComponent] = useChipBoolean('Prompt', false); const { systemPurposeId, setSystemPurposeId } = useChatStore(useShallow(state => { const conversation = state.conversations.find(conversation => conversation.id === props.conversationId); return { systemPurposeId: conversation ? conversation.systemPurposeId : null, setSystemPurposeId: conversation ? state.setSystemPurposeId : null, }; })); const { hiddenPurposeIDs, toggleHiddenPurposeId } = usePurposeStore(useShallow(state => ({ hiddenPurposeIDs: state.hiddenPurposeIDs, toggleHiddenPurposeId: state.toggleHiddenPurposeId, }))); const { domainModelId: chatLLMId } = useModelDomain('primaryChat'); const chatLLM = { id: chatLLMId ?? undefined }; // adapter for porting // derived state const isCustomPurpose = systemPurposeId === 'Custom'; const isYouTubeTranscriber = systemPurposeId === 'YouTubeTranscriber'; const { selectedPurpose, fourExamples } = React.useMemo(() => { const selectedPurpose: SystemPurposeData | null = systemPurposeId ? (SystemPurposes[systemPurposeId] ?? null) : null; // const selectedExample = selectedPurpose?.examples?.length // ? selectedPurpose.examples[Math.floor(Math.random() * selectedPurpose.examples.length)] // : null; const fourExamples = selectedPurpose?.examples?.slice(0, 4) ?? null; return { selectedPurpose, fourExamples }; }, [systemPurposeId]); const unfilteredPurposeIDs = (filteredIDs && showPersonaFinder) ? filteredIDs : Object.keys(SystemPurposes) as SystemPurposeId[]; const visiblePurposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id)); const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR); // Handlers const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => { if (purposeId && setSystemPurposeId) setSystemPurposeId(props.conversationId, purposeId); }, [props.conversationId, setSystemPurposeId]); const handleAppendTranscriptAsMessage = React.useCallback((messageText: string) => { // Create a new message object const newMessage = createDMessageTextContent('assistant', messageText); // [chat] append assistant:YouTube transcript // Append the new message to the conversation useChatStore.getState().appendMessage(props.conversationId, newMessage); }, [props.conversationId]); const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent): void => { // TODO: persist this change? Right now it's reset every time. // maybe we shall have a "save" button just save on a state to persist between sessions SystemPurposes['Custom'].systemMessage = v.target.value; }, []); const handleSwitchToCustom = React.useCallback((customText: string) => { if (setSystemPurposeId) { SystemPurposes['Custom'].systemMessage = customText; setSystemPurposeId(props.conversationId, 'Custom'); } }, [props.conversationId, setSystemPurposeId]); const toggleEditMode = React.useCallback(() => setEditMode(on => !on), []); // Search (filtering) const handleSearchClear = React.useCallback(() => { setSearchQuery(''); setFilteredIDs(null); }, []); const handleSearchOnChange = React.useCallback((e: React.ChangeEvent) => { const query = e.target.value; if (!query) return handleSearchClear(); // Filter results based on search term (title and description) const lcQuery = query.toLowerCase(); const ids = (Object.keys(SystemPurposes) as SystemPurposeId[]) .filter(key => SystemPurposes.hasOwnProperty(key)) .filter(key => { const purpose = SystemPurposes[key as SystemPurposeId]; return purpose.title.toLowerCase().includes(lcQuery) || (typeof purpose.description === 'string' && purpose.description.toLowerCase().includes(lcQuery)); }); setSearchQuery(query); setFilteredIDs(ids); // If there's a search term, activate the first item // if (ids.length && systemPurposeId && !ids.includes(systemPurposeId)) // handlePurposeChanged(ids[0] as SystemPurposeId); }, [handleSearchClear]); const handleSearchOnKeyDown = React.useCallback((e: React.KeyboardEvent): void => { if (e.key == 'Escape') handleSearchClear(); }, [handleSearchClear]); // safety check - shouldn't happen - this is set to null when the conversation is not found if (!setSystemPurposeId) return null; return ( {showPersonaFinder && } endDecorator={searchQuery && ( )} sx={{ boxShadow: 'sm', }} /> } {/* [row 0] ... Edit mode [ ] */} AI Persona {editMode ? : } {/* Personas Tiles */} {visiblePurposeIDs.map((spId: SystemPurposeId) => { const isActive = systemPurposeId === spId; const systemPurpose = SystemPurposes[spId]; return ( editMode ? toggleHiddenPurposeId(spId) : handlePurposeChanged(spId)} /> ); })} {/* Persona Creator Tile */} {(editMode || !hidePersonaCreator) && ( editMode ? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR) : void navigateToPersonas()} sx={{ fontSize: 'xs', boxShadow: 'xs', backgroundColor: 'neutral.softDisabledBg', }} /> )} {/* [row -3] Description */} {/* Description*/} {!selectedPurpose ? 'Cannot find the former persona' + (systemPurposeId ? ` "${systemPurposeId}"` : '') : selectedPurpose?.description || 'No description available'} {/* Examples/Prompt Toggles */} {fourExamples && showExamplescomponent} {!isCustomPurpose && showPromptComponent} {/* [row -3] Example incipits */} {systemPurposeId !== 'Custom' && ( {showExamples && ( {fourExamples?.map((example, idx) => ( props.runExample(example)} sx={{ justifyContent: 'space-between', borderRadius: 'md' }}> {/* Icon 📁 when the .action is 'require-data-attachment' */} {(typeof example === 'object' && example.action === 'require-data-attachment') ? '📁 ' : ''} {(typeof example === 'string') ? example : example.prompt} ))} )} {(!isCustomPurpose && showPrompt) && ( System Prompt {bareBonesPromptMixer(selectedPurpose?.systemMessage || 'No system message available', chatLLM?.id)} {!!selectedPurpose?.systemMessageNotes && ( Prompt notes: {selectedPurpose.systemMessageNotes} )} )} )} {/* [row -1] Custom Prompt box */} {systemPurposeId === 'Custom' && (