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
}
onClick={() => handleSwitchToCustom(bareBonesPromptMixer(selectedPurpose?.systemMessage || 'No system message available', chatLLM?.id))}
sx={{ ml: 'auto', my: '-0.25rem' /* absorb the button padding */ }}
>
Custom
{bareBonesPromptMixer(selectedPurpose?.systemMessage || 'No system message available', chatLLM?.id)}
{!!selectedPurpose?.systemMessageNotes && (
Prompt notes: {selectedPurpose.systemMessageNotes}
)}
)}
)}
{/* [row -1] Custom Prompt box */}
{systemPurposeId === 'Custom' && (
);
}