mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Multi-Part refactor
Partial still. Does not build.
This commit is contained in:
@@ -25,7 +25,7 @@ import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapa
|
||||
// stores access
|
||||
import { getLLMsDebugInfo } from '~/modules/llms/store-llms';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import { BeamView } from '~/modules/beam/BeamView';
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { createDConversation, createDMessage, DConversation, DMessage } from '~/common/state/store-chats';
|
||||
import { createDConversation, DConversation } from '~/common/stores/chat/chat.conversation';
|
||||
import { createDMessage, DMessage } from '~/common/stores/chat/chat.message';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { Container, Sheet } from '@mui/joy';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { useRouterQuery } from '~/common/app.routes';
|
||||
|
||||
import { CallWizard } from './CallWizard';
|
||||
|
||||
@@ -13,7 +13,7 @@ import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptim
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, Card, CardContent, Chip, IconButton, Link as MuiLink, ListDivider, MenuItem, Sheet, Switch, Typography } from '@mui/joy';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
|
||||
import { DConversation, DConversationId, conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
|
||||
import { animationShadowRingLimey } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import type { AppCallIntent } from './AppCall';
|
||||
@@ -60,7 +60,7 @@ const ContactCardConversationCall = (props: { conversation: DConversation, onCon
|
||||
function CallContactCard(props: {
|
||||
persona: MockPersona,
|
||||
callGrayUI: boolean,
|
||||
conversations: DConversation[],
|
||||
conversations: Readonly<DConversation[]>,
|
||||
setCallIntent: (intent: AppCallIntent) => void,
|
||||
}) {
|
||||
|
||||
@@ -189,7 +189,7 @@ function CallContactCard(props: {
|
||||
|
||||
|
||||
function useConversationsByPersona() {
|
||||
const conversations = useChatStore(state => state.conversations, shallow);
|
||||
const conversations = useChatStore(state => state.conversations);
|
||||
|
||||
return React.useMemo(() => {
|
||||
// group by personaId
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Card, ListDivider, ListItemDecorator, MenuItem, Switch, Typography } from '@mui/joy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
@@ -20,9 +20,11 @@ import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVo
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { createDMessage, DMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { launchAppChat, navigateToIndex } from '~/common/app.routes';
|
||||
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import type { AppCallIntent } from './AppCall';
|
||||
@@ -99,7 +101,7 @@ export function Telephone(props: {
|
||||
|
||||
// external state
|
||||
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown();
|
||||
const { chatTitle, reMessages } = useChatStore(state => {
|
||||
const { chatTitle, reMessages } = useChatStore(useShallow(state => {
|
||||
const conversation = props.callIntent.conversationId
|
||||
? state.conversations.find(conversation => conversation.id === props.callIntent.conversationId) ?? null
|
||||
: null;
|
||||
@@ -107,7 +109,7 @@ export function Telephone(props: {
|
||||
chatTitle: conversation ? conversationTitle(conversation) : null,
|
||||
reMessages: conversation ? conversation.messages : null,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
|
||||
const personaCallStarters = persona?.call?.starters ?? undefined;
|
||||
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
|
||||
@@ -181,7 +183,7 @@ export function Telephone(props: {
|
||||
// only act when we have a new user message
|
||||
if (!isConnected || callMessages.length < 1 || callMessages[callMessages.length - 1].role !== 'user')
|
||||
return;
|
||||
switch (callMessages[callMessages.length - 1].text) {
|
||||
switch (singleTextOrThrow(callMessages[callMessages.length - 1])) {
|
||||
// do not respond
|
||||
case 'Stop.':
|
||||
return;
|
||||
@@ -206,7 +208,7 @@ export function Telephone(props: {
|
||||
|
||||
// temp fix: when the chat has no messages, only assume a single system message
|
||||
const chatMessages: { role: VChatMessageIn['role'], text: string }[] = (reMessages && reMessages.length > 0)
|
||||
? reMessages
|
||||
? reMessages.map(message => ({ role: message.role, text: singleTextOrThrow(message) }))
|
||||
: personaSystemMessage
|
||||
? [{ role: 'system', text: personaSystemMessage }]
|
||||
: [];
|
||||
@@ -217,7 +219,7 @@ export function Telephone(props: {
|
||||
{ role: 'system', content: 'You are having a phone call. Your response style is brief and to the point, and according to your personality, defined below.' },
|
||||
...chatMessages.map(message => ({ role: message.role, content: message.text })),
|
||||
{ role: 'system', content: 'You are now on the phone call related to the chat above. Respect your personality and answer with short, friendly and accurate thoughtful lines.' },
|
||||
...callMessages.map(message => ({ role: message.role, content: message.text })),
|
||||
...callMessages.map(message => ({ role: message.role, content: singleTextOrThrow(message) })),
|
||||
];
|
||||
|
||||
// perform completion
|
||||
@@ -339,7 +341,7 @@ export function Telephone(props: {
|
||||
{callMessages.map((message) =>
|
||||
<CallMessage
|
||||
key={message.id}
|
||||
text={message.text}
|
||||
text={singleTextOrThrow(message)}
|
||||
variant={message.role === 'assistant' ? 'solid' : 'soft'}
|
||||
color={message.role === 'assistant' ? 'neutral' : 'primary'}
|
||||
role={message.role}
|
||||
|
||||
@@ -17,13 +17,15 @@ import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { DAttachmentPart, DMessage, DMessageMetadata, createDMessage } from '~/common/stores/chat/chat.message';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { PreferencesTab, useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageMetadata, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/store-chats';
|
||||
import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
@@ -31,7 +33,6 @@ import { useRouterQuery } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { ChatBarAltBeam } from './components/ChatBarAltBeam';
|
||||
import { ChatBarAltTitle } from './components/ChatBarAltTitle';
|
||||
import { ChatBarDropdowns } from './components/ChatBarDropdowns';
|
||||
@@ -210,9 +211,9 @@ export function AppChat() {
|
||||
return outcome === true;
|
||||
}, [openModelsSetup, openPreferencesTab]);
|
||||
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata): boolean => {
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: DAttachmentPart[], metadata?: DMessageMetadata): boolean => {
|
||||
// validate inputs
|
||||
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
|
||||
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'atext') {
|
||||
addSnackbar({
|
||||
key: 'chat-composer-action-invalid',
|
||||
message: 'Only a single text part is supported for now.',
|
||||
|
||||
@@ -5,7 +5,7 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
|
||||
import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
|
||||
import { useChatLLMDropdown } from './useLLMDropdown';
|
||||
import { usePersonaIdDropdown } from './usePersonaDropdown';
|
||||
|
||||
@@ -12,7 +12,7 @@ import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { DebounceInputMemo } from '~/common/components/DebounceInput';
|
||||
|
||||
@@ -15,10 +15,11 @@ import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import type { DFolder } from '~/common/state/store-folders';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { isDeepEqual } from '~/common/util/jsUtils';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
import { STREAM_TEXT_INDICATOR } from '../editors/chat-stream';
|
||||
|
||||
@@ -6,11 +6,13 @@ import { Box, List } from '@mui/joy';
|
||||
|
||||
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import { DMessage, DMessageUserFlag, contentPartsReplaceText, createDMessage, messageToggleUserFlag, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageUserFlag, getConversation, messageToggleUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
import { getConversation, useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useBrowserTranslationWarning } from '~/common/components/useIsBrowserTranslating';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useEphemerals } from '~/common/chats/EphemeralsStore';
|
||||
@@ -127,7 +129,9 @@ export function ChatMessageList(props: {
|
||||
}, [conversationId, deleteMessage]);
|
||||
|
||||
const handleMessageEdit = React.useCallback((messageId: string, newText: string) => {
|
||||
conversationId && editMessage(conversationId, messageId, { text: newText }, true);
|
||||
conversationId && editMessage(conversationId, messageId, (message): Partial<DMessage> => ({
|
||||
content: contentPartsReplaceText(message, newText),
|
||||
}), true);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleMessageToggleUserFlag = React.useCallback((messageId: string, userFlag: DMessageUserFlag) => {
|
||||
@@ -195,8 +199,9 @@ export function ChatMessageList(props: {
|
||||
|
||||
const { diffTargetMessage, diffPrevText } = React.useMemo(() => {
|
||||
const [msgB, msgA] = conversationMessages.filter(m => m.role === 'assistant').reverse();
|
||||
if (msgB?.text && msgA?.text && !msgB?.typing) {
|
||||
const textA = msgA.text, textB = msgB.text;
|
||||
const textB = msgB ? singleTextOrThrow(msgB) : undefined;
|
||||
const textA = msgA ? singleTextOrThrow(msgA) : undefined;
|
||||
if (textB && textA && !msgB?.typing) {
|
||||
const lenA = textA.length, lenB = textB.length;
|
||||
if (lenA > 80 && lenB > 80 && lenA > lenB / 3 && lenB > lenA / 3)
|
||||
return { diffTargetMessage: msgB, diffPrevText: textA };
|
||||
|
||||
@@ -13,7 +13,7 @@ import SettingsSuggestOutlinedIcon from '@mui/icons-material/SettingsSuggestOutl
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import VerticalSplitOutlinedIcon from '@mui/icons-material/VerticalSplitOutlined';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import type { DEphemeral } from '~/common/chats/EphemeralsStore';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { DConversationId } from '~/common/state/store-chats';
|
||||
import { DEphemeral } from '~/common/chats/EphemeralsStore';
|
||||
import { lineHeightChatTextMd } from '~/common/app.theme';
|
||||
|
||||
|
||||
|
||||
@@ -24,11 +24,13 @@ import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { DAttachmentPart, DMessageMetadata, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversationId, DMessageMetadata, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { getConversation, useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { isMacUser } from '~/common/util/pwaUtils';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
@@ -53,7 +55,6 @@ import { Attachments } from './attachments/Attachments';
|
||||
import { getSingleTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { useAttachments } from './attachments/useAttachments';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './composer.types';
|
||||
import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
@@ -101,7 +102,7 @@ export function Composer(props: {
|
||||
capabilityHasT2I: boolean;
|
||||
isMulticast: boolean | null;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata) => boolean;
|
||||
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: DAttachmentPart[], metadata?: DMessageMetadata) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
setIsMulticast: (on: boolean) => void;
|
||||
sx?: SxProps;
|
||||
@@ -129,13 +130,13 @@ export function Composer(props: {
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(useShallow(state => {
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, abortTyping } = useChatStore(useShallow(state => {
|
||||
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
|
||||
return {
|
||||
assistantAbortible: conversation ? !!conversation.abortController : false,
|
||||
systemPurposeId: conversation?.systemPurposeId ?? null,
|
||||
tokenCount: conversation ? conversation.tokenCount : 0,
|
||||
stopTyping: state.stopTyping,
|
||||
abortTyping: state.abortTyping,
|
||||
};
|
||||
}));
|
||||
const { inComposer: browsingInComposer } = useBrowseCapability();
|
||||
@@ -234,8 +235,8 @@ export function Composer(props: {
|
||||
}, [composeText, handleSendAction]);
|
||||
|
||||
const handleStopClicked = React.useCallback(() => {
|
||||
!!props.conversationId && stopTyping(props.conversationId);
|
||||
}, [props.conversationId, stopTyping]);
|
||||
!!props.conversationId && abortTyping(props.conversationId);
|
||||
}, [abortTyping, props.conversationId]);
|
||||
|
||||
|
||||
// Secondary buttons
|
||||
@@ -299,12 +300,13 @@ export function Composer(props: {
|
||||
// get the message
|
||||
const conversation = getConversation(item.conversationId);
|
||||
const messageToAttach = conversation?.messages.find(m => m.id === item.messageId);
|
||||
if (conversation && messageToAttach && messageToAttach.text) {
|
||||
const messageText = messageToAttach ? singleTextOrThrow(messageToAttach) : null;
|
||||
if (conversation && messageToAttach && messageText) {
|
||||
// Testing with this serialization for LLM. Note it will still be within a multi-part message,
|
||||
// this could be in a titled markdown block. Don't know yet how this fares with different LLMs.
|
||||
const chatTitle = conversationTitle(conversation);
|
||||
const textPlain = `---\nitem id: ${messageToAttach.id}\ncontext title: ${chatTitle}\n---\n${messageToAttach.text.trim()}\n`;
|
||||
void attachAppendEgoMessage('context-item', textPlain, `${chatTitle} > ${messageToAttach.text.slice(0, 10)}...`);
|
||||
const textPlain = `---\nitem id: ${messageToAttach.id}\ncontext title: ${chatTitle}\n---\n${messageText.trim()}\n`;
|
||||
void attachAppendEgoMessage('context-item', textPlain, `${chatTitle} > ${messageText.slice(0, 10)}...`);
|
||||
}
|
||||
}, [attachAppendEgoMessage]);
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { conversationTitle, DConversationId, messageHasUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { messageHasUserFlag, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
@@ -27,7 +29,7 @@ export function providerStarredMessage(onMessageSeelect: (item: StarredMessageIt
|
||||
messageId: message.id,
|
||||
// looks
|
||||
key: message.id,
|
||||
label: conversationTitle(conversation) + ' - ' + message.text.slice(0, 32) + '...',
|
||||
label: conversationTitle(conversation) + ' - ' + singleTextOrThrow(message).slice(0, 32) + '...',
|
||||
// description: message.text.slice(32, 100),
|
||||
Icon: undefined,
|
||||
} satisfies StarredMessageItem);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
export type ComposerOutputPartType = 'text-block' | 'image-part';
|
||||
|
||||
export type ComposerOutputPart = {
|
||||
type: 'text-block',
|
||||
text: string,
|
||||
title: string | null,
|
||||
collapsible: boolean,
|
||||
} | {
|
||||
// TODO: not implemented yet
|
||||
type: 'image-part',
|
||||
base64Url: string,
|
||||
metadata: {
|
||||
title?: string,
|
||||
generatedBy?: string,
|
||||
altText?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
},
|
||||
collapsible: false,
|
||||
};
|
||||
|
||||
export type ComposerOutputMultiPart = ComposerOutputPart[];
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { DropdownItems, PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useSanityTextDiffs } from '~/modules/blocks/RenderTextDiff';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DMessage, DMessageUserFlag, messageHasUserFlag } from '~/common/state/store-chats';
|
||||
import { DMessage, DMessageRole, DMessageUserFlag, messageHasUserFlag, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { Link } from '~/common/components/Link';
|
||||
@@ -53,7 +53,7 @@ const SELECTION_TOOLBAR_MIN_LENGTH = 3;
|
||||
const ENABLE_COPY_MESSAGE_OVERLAY: boolean = false;
|
||||
|
||||
|
||||
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, isAssistantIssue: boolean): string {
|
||||
export function messageBackground(messageRole: DMessageRole | string, wasEdited: boolean, isAssistantIssue: boolean): string {
|
||||
switch (messageRole) {
|
||||
case 'user':
|
||||
return 'primary.plainHoverBg'; // was .background.level1
|
||||
@@ -87,7 +87,7 @@ const personaSx: SxProps = {
|
||||
};
|
||||
|
||||
|
||||
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
|
||||
export function makeAvatar(messageAvatar: string | null, messageRole: DMessageRole | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | string | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
|
||||
if (typeof messageAvatar === 'string' && messageAvatar)
|
||||
return <Avatar alt={messageSender} src={messageAvatar} />;
|
||||
|
||||
@@ -124,7 +124,7 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
|
||||
}} />;
|
||||
|
||||
// purpose symbol (if present)
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
const symbol = SystemPurposes[messagePurposeId as SystemPurposeId]?.symbol;
|
||||
if (symbol)
|
||||
return <Box sx={{
|
||||
fontSize: '24px',
|
||||
@@ -256,12 +256,14 @@ export function ChatMessage(props: {
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
})));
|
||||
const [showDiff, setShowDiff] = useChatShowTextDiff();
|
||||
const textDiffs = useSanityTextDiffs(props.message.text, props.diffPreviousText, showDiff);
|
||||
|
||||
const messageText = singleTextOrThrow(props.message);
|
||||
|
||||
const textDiffs = useSanityTextDiffs(messageText, props.diffPreviousText, showDiff);
|
||||
|
||||
// derived state
|
||||
const {
|
||||
id: messageId,
|
||||
text: messageText,
|
||||
sender: messageSender,
|
||||
avatar: messageAvatar,
|
||||
typing: messageTyping,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, Button, Checkbox, IconButton, ListItem, Sheet, Typography } from '
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { TokenBadgeMemo } from '../composer/TokenBadge';
|
||||
import { makeAvatar, messageBackground } from './ChatMessage';
|
||||
@@ -44,7 +44,6 @@ export function CleanerMessage(props: { message: DMessage, selected: boolean, re
|
||||
// derived state
|
||||
const {
|
||||
id: messageId,
|
||||
text: messageText,
|
||||
sender: messageSender,
|
||||
avatar: messageAvatar,
|
||||
typing: messageTyping,
|
||||
@@ -55,6 +54,8 @@ export function CleanerMessage(props: { message: DMessage, selected: boolean, re
|
||||
updated: messageUpdated,
|
||||
} = props.message;
|
||||
|
||||
const messageText = singleTextOrThrow(props.message);
|
||||
|
||||
const fromAssistant = messageRole === 'assistant';
|
||||
|
||||
const isAssistantError = fromAssistant && (messageText.startsWith('[Issue] ') || messageText.startsWith('[OpenAI Issue]'));
|
||||
|
||||
@@ -4,7 +4,8 @@ import { persist } from 'zustand/middleware';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
|
||||
// change this to increase/decrease the number history steps per pane
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
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';
|
||||
@@ -16,10 +15,12 @@ import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
import { useChatLLM } from '~/modules/llms/store-llms';
|
||||
|
||||
import { DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { createDMessage } 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 { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -187,17 +188,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
const conversationId = props.conversationId;
|
||||
|
||||
// Create a new message object
|
||||
const newMessage: DMessage = {
|
||||
id: uuidv4(),
|
||||
text: messageText,
|
||||
sender: 'Bot',
|
||||
avatar: null,
|
||||
typing: false,
|
||||
role: 'assistant' as 'assistant',
|
||||
tokenCount: 0,
|
||||
created: Date.now(),
|
||||
updated: null,
|
||||
};
|
||||
const newMessage = createDMessage('assistant', messageText);
|
||||
|
||||
// Append the new message to the conversation
|
||||
appendMessage(conversationId, newMessage);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { DFolder } from '~/common/state/store-folders';
|
||||
import { conversationTitle, DConversationId, DMessageUserFlag, messageHasUserFlag, messageUserFlagToEmoji, useChatStore } from '~/common/state/store-chats';
|
||||
import { DMessageUserFlag, messageHasUserFlag, messageUserFlagToEmoji, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import type { ChatNavigationItemData } from './ChatDrawerItem';
|
||||
|
||||
@@ -119,7 +121,7 @@ export function useChatDrawerRenderItems(
|
||||
let searchFrequency: number = 0;
|
||||
if (isSearching) {
|
||||
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (singleTextOrThrow(message).toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
searchFrequency = titleFrequency + messageFrequency;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { usePurposeStore } from './persona-selector/store-purposes';
|
||||
|
||||
@@ -2,7 +2,9 @@ import { getChatLLMId } from '~/modules/llms/store-llms';
|
||||
import { updateHistoryForReplyTo } from '~/modules/aifn/replyto/replyTo';
|
||||
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { createDMessage, DConversationId, DMessage, getConversationSystemPurposeId } from '~/common/state/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { DMessage, createDMessage, createTextPart, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { getConversationSystemPurposeId } from '~/common/stores/chat/store-chats';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { extractChatCommand, findAllChatCommands } from '../commands/commands.registry';
|
||||
@@ -44,8 +46,9 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
|
||||
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
const lastMessageText = lastMessage ? singleTextOrThrow(lastMessage) : '';
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
const chatCommand = extractChatCommand(lastMessageText)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-browse':
|
||||
@@ -75,7 +78,7 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
content: [createTextPart(chatCommand.params || '')],
|
||||
} satisfies Partial<DMessage>);
|
||||
cHandler.messagesReplace(history);
|
||||
return true;
|
||||
@@ -130,18 +133,18 @@ export async function _handleExecute(chatModeId: ChatModeId, conversationId: DCo
|
||||
return true;
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text) break;
|
||||
if (!lastMessage || !lastMessageText) break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
text: `/draw ${lastMessageText}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessageText);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text) break;
|
||||
if (!lastMessage || !lastMessageText) break;
|
||||
cHandler.messagesReplace(history);
|
||||
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
|
||||
return await runReActUpdatingState(cHandler, lastMessageText, chatLLMId);
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
|
||||
@@ -5,7 +5,7 @@ import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, createTextPart, singleTextOrThrow, singleTextOrThrow2 } from '~/common/stores/chat/chat.message';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
|
||||
import { ChatAutoSpeakType, getChatAutoAI } from '../store-app-chat';
|
||||
@@ -33,7 +33,7 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
// stream the assistant's messages
|
||||
const messageStatus = await streamAssistantMessage(
|
||||
assistantLlmId,
|
||||
history.map((m): VChatMessageIn => ({ role: m.role, content: m.text })),
|
||||
history.map((m): VChatMessageIn => ({ role: m.role, content: singleTextOrThrow(m) })),
|
||||
parallelViewCount,
|
||||
autoSpeak,
|
||||
(update) => cHandler.messageEdit(assistantMessageId, update, false),
|
||||
@@ -89,7 +89,7 @@ export async function streamAssistantMessage(
|
||||
}
|
||||
}
|
||||
|
||||
const incrementalAnswer: Partial<DMessage> = { text: '' };
|
||||
const incrementalAnswer: Partial<DMessage> = { content: [createTextPart('')] };
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(llmId, messagesHistory, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
@@ -97,7 +97,7 @@ export async function streamAssistantMessage(
|
||||
|
||||
// grow the incremental message
|
||||
if (update.originLLM) incrementalAnswer.originLLM = update.originLLM;
|
||||
if (textSoFar) incrementalAnswer.text = textSoFar;
|
||||
if (textSoFar) incrementalAnswer.content = [createTextPart(textSoFar)];
|
||||
if (update.typing !== undefined) incrementalAnswer.typing = update.typing;
|
||||
|
||||
// Update the data store, with optional max-frequency throttling (e.g. OpenAI is downsamped 50 -> 12Hz)
|
||||
@@ -121,7 +121,8 @@ export async function streamAssistantMessage(
|
||||
if (error?.name !== 'AbortError') {
|
||||
console.error('Fetch request error:', error);
|
||||
const errorText = ` [Issue: ${error.message || (typeof error === 'string' ? error : 'Chat stopped.')}]`;
|
||||
incrementalAnswer.text = (incrementalAnswer.text || '') + errorText;
|
||||
const incrementalText = singleTextOrThrow2(incrementalAnswer.content);
|
||||
incrementalAnswer.content = [createTextPart(incrementalText + errorText)];
|
||||
returnStatus.outcome = 'errored';
|
||||
returnStatus.errorMessage = error.message;
|
||||
} else
|
||||
@@ -134,8 +135,11 @@ export async function streamAssistantMessage(
|
||||
editMessage({ ...incrementalAnswer, typing: false });
|
||||
|
||||
// 📢 TTS: all
|
||||
if ((autoSpeak === 'all' || autoSpeak === 'firstLine') && incrementalAnswer.text && !spokenLine && !abortSignal.aborted)
|
||||
void speakText(incrementalAnswer.text);
|
||||
if ((autoSpeak === 'all' || autoSpeak === 'firstLine') && !spokenLine && !abortSignal.aborted) {
|
||||
const incrementalText = singleTextOrThrow2(incrementalAnswer.content);
|
||||
if (incrementalText.length > 0)
|
||||
void speakText(incrementalText);
|
||||
}
|
||||
|
||||
return returnStatus;
|
||||
}
|
||||
@@ -13,17 +13,17 @@ import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { LogoProgress } from '~/common/components/LogoProgress';
|
||||
import { addSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle } from '~/common/state/store-chats';
|
||||
import { conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { navigateToChatLinkList } from '~/common/app.routes';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { LinkChatDrawer } from './LinkChatDrawer';
|
||||
import { LinkChatPageMenuItems } from './LinkChatPageMenuItems';
|
||||
import { LinkChatViewer } from './LinkChatViewer';
|
||||
import { addSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { navigateToChatLinkList } from '~/common/app.routes';
|
||||
|
||||
|
||||
const SPECIAL_LIST_PAGE_ID = 'list';
|
||||
|
||||
@@ -8,11 +8,13 @@ import { ChatMessageMemo } from '../chat/components/message/ChatMessage';
|
||||
import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { DConversation, conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { createTextPart, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { launchAppChat } from '~/common/app.routes';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -42,7 +44,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
React.useEffect(() => {
|
||||
const { renderMarkdown, setRenderMarkdown } = useUIPreferencesStore.getState();
|
||||
if (!renderMarkdown) {
|
||||
const hasMarkdownTables = messages.some(m => m.text.includes('|---'));
|
||||
const hasMarkdownTables = messages.some(m => singleTextOrThrow(m).includes('|---'));
|
||||
if (hasMarkdownTables) {
|
||||
setRenderMarkdown(true);
|
||||
console.log('Turning on Markdown because of tables');
|
||||
@@ -136,7 +138,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
message={message}
|
||||
fitScreen={isMobile}
|
||||
showBlocksDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
onMessageEdit={(_messageId, text: string) => message.text = text}
|
||||
onMessageEdit={(_messageId, text: string) => message.content = [createTextPart(text)]}
|
||||
/>,
|
||||
)}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ import Router, { useRouter } from 'next/router';
|
||||
import type { AppCallIntent } from '../apps/call/AppCall';
|
||||
import type { AppChatIntent } from '../apps/chat/AppChat';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
|
||||
import { isBrowser } from './util/pwaUtils';
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
|
||||
import { ChatActions, createDMessage, DConversationId, DMessage, getConversationSystemPurposeId, useChatStore } from '../state/store-chats';
|
||||
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
|
||||
import { ChatActions, getConversationSystemPurposeId, useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { createDMessage, createTextPart, DMessage, fixmeThisReplacesAllParts } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { EphemeralHandler, EphemeralsStore } from './EphemeralsStore';
|
||||
import { createChatOverlayVanillaStore } from './store-chat-overlay-vanilla';
|
||||
|
||||
@@ -40,7 +42,8 @@ export class ConversationHandler {
|
||||
let systemMessage: DMessage = systemMessageIndex >= 0 ? history.splice(systemMessageIndex, 1)[0] : createDMessage('system', '');
|
||||
if (!systemMessage.updated && purposeId && SystemPurposes[purposeId]?.systemMessage) {
|
||||
systemMessage.purposeId = purposeId;
|
||||
systemMessage.text = bareBonesPromptMixer(SystemPurposes[purposeId].systemMessage, assistantLlmId);
|
||||
const systemMessageText = bareBonesPromptMixer(SystemPurposes[purposeId].systemMessage, assistantLlmId);
|
||||
systemMessage.content = [createTextPart(systemMessageText)];
|
||||
|
||||
// HACK: this is a special case for the 'Custom' persona, to set the message in stone (so it doesn't get updated when switching to another persona)
|
||||
if (purposeId === 'Custom')
|
||||
@@ -68,7 +71,7 @@ export class ConversationHandler {
|
||||
* @param purposeId purpose that supposedly triggered this message
|
||||
* @param typing whether the assistant is typing at the onset
|
||||
*/
|
||||
messageAppendAssistant(text: string, purposeId: SystemPurposeId | undefined, llmLabel: DLLMId | string, typing: boolean): string {
|
||||
messageAppendAssistant(text: string, purposeId: SystemPurposeId | string | undefined, llmLabel: DLLMId | string, typing: boolean): string {
|
||||
const assistantMessage: DMessage = createDMessage('assistant', text);
|
||||
assistantMessage.typing = typing;
|
||||
assistantMessage.purposeId = purposeId ?? undefined;
|
||||
@@ -108,7 +111,7 @@ export class ConversationHandler {
|
||||
// set output when going back to the chat
|
||||
if (destReplaceMessageId) {
|
||||
// replace a single message in the conversation history
|
||||
this.messageEdit(destReplaceMessageId, { text: messageText, originLLM: llmId }, true);
|
||||
this.messageEdit(destReplaceMessageId, { content: fixmeThisReplacesAllParts(messageText), originLLM: llmId }, true);
|
||||
} else {
|
||||
// replace (may truncate) the conversation history and append a message
|
||||
const newMessage = createDMessage('assistant', messageText);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
|
||||
import { ConversationHandler } from './ConversationHandler';
|
||||
|
||||
|
||||
/**
|
||||
* Singleton to get a global instance related to a conversationId. Note we don't have reference counting, and mainly because we cannot
|
||||
* do comprehensive lifecycle tracking.
|
||||
|
||||
@@ -1,513 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DLLMId, getChatLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import { idbStateStorage } from '../util/idbUtils';
|
||||
import { countModelTokens } from '../util/token-counter';
|
||||
import { defaultSystemPurposeId, SystemPurposeId } from '../../data';
|
||||
|
||||
|
||||
export type DConversationId = string;
|
||||
|
||||
/**
|
||||
* Conversation, a list of messages between humans and bots
|
||||
* Future:
|
||||
* - draftUserMessage?: { text: string; attachments: any[] };
|
||||
* - isMuted: boolean; isArchived: boolean; Starred: boolean; participants: string[];
|
||||
*/
|
||||
export interface DConversation {
|
||||
id: DConversationId;
|
||||
messages: DMessage[];
|
||||
systemPurposeId: SystemPurposeId;
|
||||
userTitle?: string;
|
||||
autoTitle?: string;
|
||||
tokenCount: number; // f(messages, llmId)
|
||||
created: number; // created timestamp (Date.now())
|
||||
updated: number | null; // updated timestamp (Date.now())
|
||||
// Not persisted, used while in-memory, or temporarily by the UI
|
||||
abortController: AbortController | null;
|
||||
}
|
||||
|
||||
export function createDConversation(systemPurposeId?: SystemPurposeId): DConversation {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
messages: [],
|
||||
systemPurposeId: systemPurposeId || defaultSystemPurposeId,
|
||||
tokenCount: 0,
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
abortController: null,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultConversations: DConversation[] = [createDConversation()];
|
||||
|
||||
/**
|
||||
* Message, sent or received, by humans or bots
|
||||
*
|
||||
* Other ideas:
|
||||
* - attachments?: {type: string; url: string; thumbnailUrl?: string; size?: number}[];
|
||||
* - isPinned?: boolean;
|
||||
* - reactions?: {type: string; count: number; users: string[]}[];
|
||||
* - status: 'sent' | 'delivered' | 'read' | 'failed';
|
||||
*/
|
||||
export interface DMessage {
|
||||
id: string;
|
||||
text: string;
|
||||
sender: 'You' | 'Bot' | string; // pretty name
|
||||
avatar: string | null; // null, or image url
|
||||
typing: boolean;
|
||||
role: 'assistant' | 'system' | 'user';
|
||||
|
||||
purposeId?: SystemPurposeId; // only assistant/system
|
||||
originLLM?: string; // only assistant - model that generated this message, goes beyond known models
|
||||
|
||||
metadata?: DMessageMetadata; // metadata, mainly at creation and for UI
|
||||
userFlags?: DMessageUserFlag[]; // (UI) user-set per-message flags
|
||||
|
||||
tokenCount: number; // cache for token count, using the current Conversation model (0 = not yet calculated)
|
||||
|
||||
created: number; // created timestamp
|
||||
updated: number | null; // updated timestamp
|
||||
}
|
||||
|
||||
export type DMessageUserFlag =
|
||||
| 'starred'; // user starred this
|
||||
|
||||
export interface DMessageMetadata {
|
||||
inReplyToText?: string; // text this was in reply to
|
||||
}
|
||||
|
||||
export function createDMessage(role: DMessage['role'], text: string): DMessage {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
text,
|
||||
sender: role === 'user' ? 'You' : 'Bot',
|
||||
avatar: null,
|
||||
typing: false,
|
||||
role: role,
|
||||
tokenCount: 0,
|
||||
created: Date.now(),
|
||||
updated: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function messageHasUserFlag(message: DMessage, flag: DMessageUserFlag): boolean {
|
||||
return message.userFlags?.includes(flag) ?? false;
|
||||
}
|
||||
|
||||
export function messageToggleUserFlag(message: DMessage, flag: DMessageUserFlag): DMessageUserFlag[] {
|
||||
if (message.userFlags?.includes(flag))
|
||||
return message.userFlags.filter(_f => _f !== flag);
|
||||
else
|
||||
return [...(message.userFlags || []), flag];
|
||||
}
|
||||
|
||||
const dMessageUserFlagToEmojiMap: Record<DMessageUserFlag, string> = {
|
||||
starred: '⭐️',
|
||||
};
|
||||
|
||||
export function messageUserFlagToEmoji(flag: DMessageUserFlag): string {
|
||||
return dMessageUserFlagToEmojiMap[flag] || '❓';
|
||||
}
|
||||
|
||||
|
||||
/// Conversations Store
|
||||
|
||||
interface ChatState {
|
||||
conversations: DConversation[];
|
||||
}
|
||||
|
||||
export interface ChatActions {
|
||||
// store setters
|
||||
prependNewConversation: (personaId: SystemPurposeId | undefined) => DConversationId;
|
||||
importConversation: (conversation: DConversation, preventClash: boolean) => DConversationId;
|
||||
branchConversation: (conversationId: DConversationId, messageId: string | null) => DConversationId | null;
|
||||
deleteConversations: (conversationIds: DConversationId[], newConversationPersonaId?: SystemPurposeId) => DConversationId;
|
||||
|
||||
// within a conversation
|
||||
setAbortController: (conversationId: string, abortController: AbortController | null) => void;
|
||||
stopTyping: (conversationId: string) => void;
|
||||
setMessages: (conversationId: string, messages: DMessage[]) => void;
|
||||
appendMessage: (conversationId: string, message: DMessage) => void;
|
||||
deleteMessage: (conversationId: string, messageId: string) => void;
|
||||
editMessage: (conversationId: string, messageId: string, update: Partial<DMessage> | ((message: DMessage) => Partial<DMessage>), touchUpdated: boolean) => void;
|
||||
updateMetadata: (conversationId: string, messageId: string, metadataDelta: Partial<DMessageMetadata>, touchUpdated?: boolean) => void;
|
||||
setSystemPurposeId: (conversationId: string, systemPurposeId: SystemPurposeId) => void;
|
||||
setAutoTitle: (conversationId: string, autoTitle: string) => void;
|
||||
setUserTitle: (conversationId: string, userTitle: string) => void;
|
||||
|
||||
// utility function
|
||||
_editConversation: (conversationId: string, update: Partial<DConversation> | ((conversation: DConversation) => Partial<DConversation>)) => void;
|
||||
}
|
||||
|
||||
type ConversationsStore = ChatState & ChatActions;
|
||||
|
||||
export const useChatStore = create<ConversationsStore>()(devtools(
|
||||
persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
// default state
|
||||
conversations: defaultConversations,
|
||||
|
||||
prependNewConversation: (personaId: SystemPurposeId | undefined): DConversationId => {
|
||||
const newConversation = createDConversation(personaId);
|
||||
|
||||
_set(state => ({
|
||||
conversations: [
|
||||
newConversation,
|
||||
...state.conversations,
|
||||
],
|
||||
}));
|
||||
|
||||
return newConversation.id;
|
||||
},
|
||||
|
||||
importConversation: (conversation: DConversation, preventClash: boolean): DConversationId => {
|
||||
const { conversations } = _get();
|
||||
|
||||
// if there's a clash, abort the former conversation, and optionally change the ID
|
||||
const existing = conversations.find(_c => _c.id === conversation.id);
|
||||
if (existing) {
|
||||
existing?.abortController?.abort();
|
||||
if (preventClash) {
|
||||
conversation.id = uuidv4();
|
||||
console.warn('Conversation ID clash, changing ID to', conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
conversation.tokenCount = updateTokenCounts(conversation.messages, true, 'importConversation');
|
||||
|
||||
_set({
|
||||
conversations: [
|
||||
conversation,
|
||||
...conversations.filter(_c => _c.id !== conversation.id),
|
||||
],
|
||||
});
|
||||
|
||||
return conversation.id;
|
||||
},
|
||||
|
||||
branchConversation: (conversationId: DConversationId, messageId: string | null): DConversationId | null => {
|
||||
const { conversations } = _get();
|
||||
const conversation = conversations.find(_c => _c.id === conversationId);
|
||||
if (!conversation)
|
||||
return null;
|
||||
|
||||
// create a deep copy of the conversation
|
||||
const deepCopy: DConversation = JSON.parse(JSON.stringify(conversation));
|
||||
let messageIndex = deepCopy.messages.length; // By default, include all messages if messageId is null
|
||||
if (messageId !== null) {
|
||||
messageIndex = deepCopy.messages.findIndex(_m => _m.id === messageId);
|
||||
messageIndex = messageIndex >= 0 ? messageIndex + 1 : deepCopy.messages.length; // If message is found, include it
|
||||
}
|
||||
|
||||
// title this branched chat differently
|
||||
const newTitle = getNextBranchTitle(conversationTitle(conversation));
|
||||
|
||||
const branched: DConversation = {
|
||||
...deepCopy,
|
||||
id: uuidv4(), // roll conversation ID
|
||||
messages: deepCopy.messages
|
||||
.slice(0, messageIndex)
|
||||
.map((message: DMessage): DMessage => ({
|
||||
...message,
|
||||
id: uuidv4(), // roll message ID
|
||||
typing: false,
|
||||
})),
|
||||
updated: Date.now(),
|
||||
// Set the new title for the branched conversation
|
||||
autoTitle: newTitle,
|
||||
// reset transient
|
||||
abortController: null,
|
||||
// TODO: set references to parent conversation & message?
|
||||
};
|
||||
|
||||
_set({
|
||||
conversations: [
|
||||
branched,
|
||||
...conversations,
|
||||
],
|
||||
});
|
||||
|
||||
return branched.id;
|
||||
},
|
||||
|
||||
deleteConversations: (conversationIds: DConversationId[], newConversationPersonaId?: SystemPurposeId): DConversationId => {
|
||||
let { conversations } = _get();
|
||||
|
||||
// find the index of first conversation to delete
|
||||
const cIndex = conversationIds.length > 0 ? conversations.findIndex(_c => _c.id === conversationIds[0]) : -1;
|
||||
|
||||
// abort all pending requests
|
||||
conversationIds.forEach(conversationId => conversations.find(_c => _c.id === conversationId)?.abortController?.abort());
|
||||
|
||||
// remove from the list
|
||||
conversations = conversations.filter(_c => !conversationIds.includes(_c.id));
|
||||
|
||||
// create a new conversation if there are no more
|
||||
if (!conversations.length)
|
||||
conversations.push(createDConversation(newConversationPersonaId));
|
||||
|
||||
_set({
|
||||
conversations,
|
||||
});
|
||||
|
||||
// return the next conversation Id in line, if valid
|
||||
return conversations[(cIndex >= 0 && cIndex < conversations.length) ? cIndex : 0].id;
|
||||
},
|
||||
|
||||
|
||||
// within a conversation
|
||||
|
||||
_editConversation: (conversationId: string, update: Partial<DConversation> | ((conversation: DConversation) => Partial<DConversation>)) =>
|
||||
_set(state => ({
|
||||
conversations: state.conversations.map((conversation: DConversation): DConversation =>
|
||||
conversation.id === conversationId
|
||||
? {
|
||||
...conversation,
|
||||
...(typeof update === 'function' ? update(conversation) : update),
|
||||
}
|
||||
: conversation),
|
||||
})),
|
||||
|
||||
setAbortController: (conversationId: string, abortController: AbortController | null) =>
|
||||
_get()._editConversation(conversationId, () =>
|
||||
({
|
||||
abortController: abortController,
|
||||
})),
|
||||
|
||||
stopTyping: (conversationId: string) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
conversation.abortController?.abort();
|
||||
return {
|
||||
abortController: null,
|
||||
};
|
||||
}),
|
||||
|
||||
setMessages: (conversationId: string, newMessages: DMessage[]) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
conversation.abortController?.abort();
|
||||
return {
|
||||
messages: newMessages,
|
||||
...(!!newMessages.length ? {} : {
|
||||
autoTitle: undefined,
|
||||
}),
|
||||
tokenCount: updateTokenCounts(newMessages, false, 'setMessages'),
|
||||
updated: Date.now(),
|
||||
abortController: null,
|
||||
};
|
||||
}),
|
||||
|
||||
appendMessage: (conversationId: string, message: DMessage) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
if (!message.typing)
|
||||
updateTokenCounts([message], true, 'appendMessage');
|
||||
|
||||
const messages = [...conversation.messages, message];
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: Date.now(),
|
||||
};
|
||||
}),
|
||||
|
||||
deleteMessage: (conversationId: string, messageId: string) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
const messages = conversation.messages.filter(message => message.id !== messageId);
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: Date.now(),
|
||||
};
|
||||
}),
|
||||
|
||||
editMessage: (conversationId: string, messageId: string, update: Partial<DMessage> | ((message: DMessage) => Partial<DMessage>), touchUpdated: boolean) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
const chatLLMId = getChatLLMId();
|
||||
const messages = conversation.messages.map((message: DMessage): DMessage => {
|
||||
if (message.id === messageId) {
|
||||
const updatedMessage = typeof update === 'function' ? update(message) : update;
|
||||
return {
|
||||
...message,
|
||||
...updatedMessage,
|
||||
...(touchUpdated && { updated: Date.now() }),
|
||||
...(((updatedMessage.typing === false || !message.typing) && chatLLMId && {
|
||||
tokenCount: countModelTokens(updatedMessage.text || message.text, chatLLMId, 'editMessage(typing=false)') ?? 0,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
}),
|
||||
|
||||
updateMetadata: (conversationId: string, messageId: string, metadataDelta: Partial<DMessageMetadata>, touchUpdated: boolean = true) => {
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
const messages = conversation.messages.map(message =>
|
||||
message.id !== messageId ? message
|
||||
: {
|
||||
...message,
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
...metadataDelta,
|
||||
},
|
||||
updated: touchUpdated ? Date.now() : message.updated,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
messages,
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSystemPurposeId: (conversationId: string, systemPurposeId: SystemPurposeId) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
systemPurposeId,
|
||||
}),
|
||||
|
||||
setAutoTitle: (conversationId: string, autoTitle: string) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
autoTitle,
|
||||
}),
|
||||
|
||||
setUserTitle: (conversationId: string, userTitle: string) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
userTitle,
|
||||
}),
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'app-chats',
|
||||
/* Version history:
|
||||
* - 1: [2023-03-18] App launch, single chat
|
||||
* - 2: [2023-04-10] Multi-chat version - invalidating data to be sure
|
||||
* - 3: [2023-09-19] Switch to IndexedDB - no data shape change,
|
||||
* but we swapped the backend (localStorage -> IndexedDB)
|
||||
*/
|
||||
version: 3,
|
||||
storage: createJSONStorage(() => idbStateStorage),
|
||||
|
||||
// Migrations
|
||||
migrate: (persistedState: unknown, _fromVersion: number): ConversationsStore => {
|
||||
|
||||
// other: just proceed
|
||||
return persistedState as any;
|
||||
},
|
||||
|
||||
// Pre-Saving: remove transient properties
|
||||
partialize: (state) => ({
|
||||
...state,
|
||||
conversations: state.conversations.map((conversation: DConversation) => {
|
||||
const {
|
||||
abortController,
|
||||
...rest
|
||||
} = conversation;
|
||||
return rest;
|
||||
}),
|
||||
}),
|
||||
|
||||
// Post-Loading: re-add transient properties and cleanup state
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return;
|
||||
|
||||
// fixup state
|
||||
for (const conversation of (state.conversations || [])) {
|
||||
// reset the typing flag
|
||||
for (const message of conversation.messages)
|
||||
message.typing = false;
|
||||
|
||||
// rehydrate the transient properties
|
||||
conversation.abortController = null;
|
||||
}
|
||||
},
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'AppChats',
|
||||
enabled: false,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
export const conversationTitle = (conversation: DConversation, fallback?: string): string =>
|
||||
conversation.userTitle || conversation.autoTitle || fallback || ''; // 👋💬🗨️
|
||||
|
||||
function getNextBranchTitle(currentTitle: string): string {
|
||||
const numberPrefixRegex = /^\((\d+)\)\s+/; // Regex to find "(number) " at the beginning of the title
|
||||
const match = currentTitle.match(numberPrefixRegex);
|
||||
|
||||
if (match) {
|
||||
const number = parseInt(match[1], 10) + 1;
|
||||
return currentTitle.replace(numberPrefixRegex, `(${number}) `);
|
||||
} else
|
||||
return `(1) ${currentTitle}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convenience function to count the tokens in a DMessage object
|
||||
*/
|
||||
function updateDMessageTokenCount(message: DMessage, llmId: DLLMId | null, forceUpdate: boolean, debugFrom: string): number {
|
||||
if (forceUpdate || !message.tokenCount)
|
||||
message.tokenCount = llmId ? countModelTokens(message.text, llmId, debugFrom) ?? 0 : 0;
|
||||
return message.tokenCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to update a set of messages, using the current chatLLM
|
||||
*/
|
||||
function updateTokenCounts(messages: DMessage[], forceUpdate: boolean, debugFrom: string): number {
|
||||
const chatLLMId = getChatLLMId();
|
||||
return 3 + messages.reduce((sum, message) => 4 + updateDMessageTokenCount(message, chatLLMId, forceUpdate, debugFrom) + sum, 0);
|
||||
}
|
||||
|
||||
export const getConversation = (conversationId: DConversationId | null): DConversation | null =>
|
||||
conversationId ? useChatStore.getState().conversations.find(_c => _c.id === conversationId) ?? null : null;
|
||||
|
||||
export const getConversationSystemPurposeId = (conversationId: DConversationId | null): SystemPurposeId | null =>
|
||||
getConversation(conversationId)?.systemPurposeId || null;
|
||||
|
||||
export const useConversation = (conversationId: DConversationId | null) => useChatStore(state => {
|
||||
const { conversations } = state;
|
||||
|
||||
// this object will change if any sub-prop changes as well
|
||||
const conversation = conversationId ? conversations.find(_c => _c.id === conversationId) ?? null : null;
|
||||
const title = conversation ? conversationTitle(conversation) : null;
|
||||
const isEmpty = conversation ? !conversation.messages.length : true;
|
||||
const isDeveloper = conversation?.systemPurposeId === 'Developer';
|
||||
const conversationIdx = conversation ? conversations.findIndex(_c => _c.id === conversation.id) : -1;
|
||||
|
||||
const hasConversations = conversations.length > 1 || (conversations.length === 1 && !!conversations[0].messages.length);
|
||||
const recycleNewConversationId = (conversations.length && !conversations[0].messages.length) ? conversations[0].id : null;
|
||||
|
||||
return {
|
||||
title,
|
||||
isEmpty,
|
||||
isDeveloper,
|
||||
conversationIdx,
|
||||
hasConversations,
|
||||
recycleNewConversationId,
|
||||
prependNewConversation: state.prependNewConversation,
|
||||
branchConversation: state.branchConversation,
|
||||
deleteConversations: state.deleteConversations,
|
||||
};
|
||||
}, shallow);
|
||||
@@ -2,7 +2,7 @@ import { create } from 'zustand';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { DConversationId } from './store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
|
||||
|
||||
export interface DFolder {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { defaultSystemPurposeId, SystemPurposeId } from '../../../data';
|
||||
|
||||
import { DMessage, DMessageId, convertDMessage_V3_V4, duplicateDMessage } from './chat.message';
|
||||
|
||||
|
||||
/// Conversation
|
||||
|
||||
export interface DConversation {
|
||||
id: DConversationId; // unique identifier for this conversation
|
||||
|
||||
messages: DMessage[]; // linear list of messages in this conversation
|
||||
|
||||
// editable
|
||||
userTitle?: string;
|
||||
autoTitle?: string;
|
||||
|
||||
// TODO: @deprecated - this should be the system purpose of current head of the conversation
|
||||
// there should be the concept of the audience of the current head
|
||||
systemPurposeId: SystemPurposeId; // system purpose of this conversation
|
||||
|
||||
// TODO: @deprecated - should be a view-related cache
|
||||
tokenCount: number; // f(messages, llmId)
|
||||
|
||||
// when updated is null, we don't have messages yet (timestamps as Date.now())
|
||||
created: number; // creation timestamp
|
||||
updated: number | null; // last update timestamp
|
||||
|
||||
// Not persisted, used while in-memory, or temporarily by the UI
|
||||
// TODO: @deprecated - shouls not be in here - it's actually a per-message/operation thing
|
||||
abortController: AbortController | null;
|
||||
|
||||
// future additions:
|
||||
// draftUserMessage?: { text: string; attachments: any[] };
|
||||
// isMuted: boolean; isArchived: boolean; isStarred: boolean;
|
||||
// participants: personaIds...[];
|
||||
}
|
||||
|
||||
export type DConversationId = string;
|
||||
|
||||
|
||||
// helpers - creation
|
||||
|
||||
export function createDConversation(systemPurposeId?: SystemPurposeId): DConversation {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
|
||||
messages: [],
|
||||
|
||||
// absent
|
||||
// userTitle: undefined,
|
||||
// autoTitle: undefined,
|
||||
|
||||
// @deprecated
|
||||
systemPurposeId: systemPurposeId || defaultSystemPurposeId,
|
||||
// @deprecated
|
||||
tokenCount: 0,
|
||||
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
|
||||
abortController: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function duplicateCConversation(conversation: DConversation, lastMessageId?: DMessageId): DConversation {
|
||||
|
||||
// cut short messages, if requested
|
||||
let messagesToKeep = conversation.messages.length; // By default, include all messages if messageId is null
|
||||
if (lastMessageId) {
|
||||
const messageIndex = conversation.messages.findIndex(_m => _m.id === lastMessageId);
|
||||
if (messageIndex >= 0)
|
||||
messagesToKeep = messageIndex + 1;
|
||||
}
|
||||
|
||||
// auto-increment title (1)
|
||||
const newTitle = getNextBranchTitle(conversationTitle(conversation));
|
||||
|
||||
return {
|
||||
id: uuidv4(),
|
||||
|
||||
messages: conversation.messages
|
||||
.slice(0, messagesToKeep)
|
||||
.map(duplicateDMessage),
|
||||
|
||||
// userTitle: conversation.userTitle,
|
||||
autoTitle: newTitle,
|
||||
|
||||
systemPurposeId: conversation.systemPurposeId,
|
||||
tokenCount: conversation.tokenCount,
|
||||
|
||||
created: conversation.created,
|
||||
updated: Date.now(),
|
||||
|
||||
abortController: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// helpers - conversion
|
||||
|
||||
export function convertCConversation_V3_V4(conversation: DConversation) {
|
||||
conversation.messages.forEach(message => convertDMessage_V3_V4(message));
|
||||
}
|
||||
|
||||
|
||||
// helpers - title
|
||||
|
||||
export const conversationTitle = (conversation: DConversation, fallback?: string): string =>
|
||||
conversation.userTitle || conversation.autoTitle || fallback || ''; // 👋💬🗨️
|
||||
|
||||
function getNextBranchTitle(currentTitle: string): string {
|
||||
const numberPrefixRegex = /^\((\d+)\)\s+/; // Regex to find "(number) " at the beginning of the title
|
||||
const match = currentTitle.match(numberPrefixRegex);
|
||||
|
||||
if (match) {
|
||||
const number = parseInt(match[1], 10) + 1;
|
||||
return currentTitle.replace(numberPrefixRegex, `(${number}) `);
|
||||
} else
|
||||
return `(1) ${currentTitle}`;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { DBlobId } from '~/modules/dblobs/dblobs.types';
|
||||
|
||||
|
||||
// Message
|
||||
|
||||
export interface DMessage {
|
||||
id: DMessageId; // unique message ID
|
||||
|
||||
role: DMessageRole;
|
||||
content: DContentPart[]; // multi-part content (sent: mix of text/images/etc., received: usually one part)
|
||||
userAttachments: DAttachmentPart[]; // higher-level multi-part to be sent (transformed to multipart before sending)
|
||||
|
||||
// transient state
|
||||
typing: boolean; // incomplete message, still typing - also suspends counting tokens while true
|
||||
|
||||
// identity
|
||||
avatar: string | null; // image URL, or null
|
||||
sender: 'You' | 'Bot' | string; // pretty name @deprecated
|
||||
|
||||
purposeId?: string; // only assistant/system
|
||||
originLLM?: string; // only assistant - model that generated this message, goes beyond known models
|
||||
|
||||
metadata?: DMessageMetadata; // metadata, mainly at creation and for UI
|
||||
userFlags?: DMessageUserFlag[]; // (UI) user-set per-message flags
|
||||
|
||||
// TODO: @deprecated remove this, it's really view-dependent
|
||||
tokenCount: number; // cache for token count, using the current Conversation model (0 = not yet calculated)
|
||||
|
||||
created: number; // created timestamp
|
||||
updated: number | null; // updated timestamp
|
||||
}
|
||||
|
||||
export type DMessageId = string;
|
||||
|
||||
export type DMessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
|
||||
// Content Reference - we use a Ref and the DBlob framework to store media locally, or remote URLs
|
||||
|
||||
type DContentRef =
|
||||
| { type: 'url'; url: string } // remotely accessible URL
|
||||
| { type: 'dblob'; mimeType: string; dblobId: DBlobId } // reference to a DBlob
|
||||
;
|
||||
|
||||
// type CMediaSourceInline =
|
||||
// | { type: 'base64'; mimeType: string; base64Data: string }
|
||||
// ;
|
||||
|
||||
|
||||
// Content Part
|
||||
|
||||
export type DContentPart =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; mimeType: string; source: DContentRef }
|
||||
// | { type: 'audio'; mimeType: string; source: DContentRef }
|
||||
// | { type: 'video'; mimeType: string; source: DContentRef }
|
||||
// | { type: 'document'; source: DContentRef }
|
||||
| { type: 'function_call'; function: string; args: Record<string, any> }
|
||||
| { type: 'function_response'; function: string; response: Record<string, any> }
|
||||
;
|
||||
|
||||
|
||||
// Attachment Part
|
||||
|
||||
export type DAttachmentPart =
|
||||
| { type: 'atext', text: string, title?: string, collapsible: boolean }
|
||||
| { type: 'aimage', source: DContentRef, title?: string, width?: number, height?: number, collapsible: false }
|
||||
|
||||
// export type CAttachmentMultiPart = CAttachmentPart[];
|
||||
|
||||
|
||||
// Metadata
|
||||
|
||||
export interface DMessageMetadata {
|
||||
inReplyToText?: string; // text this was in reply to
|
||||
}
|
||||
|
||||
|
||||
// User Flags
|
||||
|
||||
export type DMessageUserFlag =
|
||||
| 'starred'; // user starred this
|
||||
|
||||
|
||||
// helpers - creation
|
||||
|
||||
export function createDMessage(role: DMessageRole, text: string): DMessage {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
|
||||
role: role,
|
||||
content: [createTextPart(text)],
|
||||
userAttachments: [],
|
||||
|
||||
// transient
|
||||
typing: false,
|
||||
|
||||
// identity
|
||||
avatar: null,
|
||||
sender: role === 'user' ? 'You' : 'Bot',
|
||||
|
||||
// absent
|
||||
// purposeId: undefined,
|
||||
// originLLM: undefined,
|
||||
// metadata: undefined,
|
||||
// userFlags: undefined,
|
||||
|
||||
// @deprecated
|
||||
tokenCount: 0,
|
||||
|
||||
// when updated is null, the message is considered incomplete yet (probably still typing)
|
||||
created: Date.now(),
|
||||
updated: null,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTextPart(text: string): DContentPart {
|
||||
return { type: 'text', text };
|
||||
}
|
||||
|
||||
|
||||
export function duplicateDMessage(message: DMessage): DMessage {
|
||||
// TODO: deep copy of content and userAttachments?
|
||||
// there may be refs to the same DBlob, but that's fine, hopefully (they are immutable once here)
|
||||
// the dblob may need more reference count?
|
||||
return {
|
||||
id: uuidv4(),
|
||||
|
||||
role: message.role,
|
||||
content: message.content.map(part => ({ ...part })),
|
||||
userAttachments: message.userAttachments.map(part => ({ ...part })),
|
||||
|
||||
typing: false,
|
||||
|
||||
avatar: message.avatar,
|
||||
sender: message.sender,
|
||||
|
||||
purposeId: message.purposeId,
|
||||
originLLM: message.originLLM,
|
||||
metadata: message.metadata ? { ...message.metadata } : undefined,
|
||||
userFlags: message.userFlags ? [...message.userFlags] : undefined,
|
||||
|
||||
tokenCount: message.tokenCount,
|
||||
|
||||
created: message.created,
|
||||
updated: message.updated,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// helpers - conversion
|
||||
|
||||
export function convertDMessage_V3_V4(message: DMessage) {
|
||||
const v3 = message as DMessage & { text: string };
|
||||
if (!message.content || message.content.length === 0) {
|
||||
message.content = [createTextPart(v3.text || '')];
|
||||
delete (v3 as any).text;
|
||||
}
|
||||
if (!message.userAttachments?.length)
|
||||
message.userAttachments = [];
|
||||
}
|
||||
|
||||
|
||||
// helpers - text
|
||||
|
||||
export function singleTextOrThrow(message: DMessage): string {
|
||||
if (message.content.length !== 1)
|
||||
throw new Error('Expected single content');
|
||||
if (message.content[0].type !== 'text')
|
||||
throw new Error('Expected text content');
|
||||
return message.content[0].text;
|
||||
}
|
||||
|
||||
export function singleTextOrThrow2(content?: DContentPart[]): string {
|
||||
if (!content || content.length !== 1)
|
||||
throw new Error('Expected single content');
|
||||
if (content[0].type !== 'text')
|
||||
throw new Error('Expected text content');
|
||||
return content[0].text;
|
||||
}
|
||||
|
||||
// zustand-like deep replace
|
||||
export function contentPartsReplaceText(message: DMessage, newText: string): DContentPart[] {
|
||||
const lastTextPart = message.content.findLast(part => part.type === 'text');
|
||||
if (!lastTextPart)
|
||||
return [...message.content, createTextPart(newText)];
|
||||
return message.content.map(part =>
|
||||
(part === lastTextPart)
|
||||
? { ...part, text: newText }
|
||||
: part,
|
||||
);
|
||||
}
|
||||
|
||||
export function fixmeThisReplacesAllParts(text: string): DContentPart[] {
|
||||
return [createTextPart(text)];
|
||||
}
|
||||
|
||||
|
||||
// helpers - user flags
|
||||
|
||||
const flag2EmojiMap: Record<DMessageUserFlag, string> = {
|
||||
starred: '⭐️',
|
||||
};
|
||||
|
||||
export function messageUserFlagToEmoji(flag: DMessageUserFlag): string {
|
||||
return flag2EmojiMap[flag] || '❓';
|
||||
}
|
||||
|
||||
export function messageHasUserFlag(message: DMessage, flag: DMessageUserFlag): boolean {
|
||||
return message.userFlags?.includes(flag) ?? false;
|
||||
}
|
||||
|
||||
export function messageToggleUserFlag(message: DMessage, flag: DMessageUserFlag): DMessageUserFlag[] {
|
||||
if (message.userFlags?.includes(flag))
|
||||
return message.userFlags.filter(_f => _f !== flag);
|
||||
else
|
||||
return [...(message.userFlags || []), flag];
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DLLMId, getChatLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import { SystemPurposeId } from '../../../data';
|
||||
import { countModelTokens } from '../../util/token-counter';
|
||||
import { idbStateStorage } from '../../util/idbUtils';
|
||||
|
||||
import { DConversation, DConversationId, conversationTitle, convertCConversation_V3_V4, createDConversation, duplicateCConversation } from './chat.conversation';
|
||||
import { DMessage, DMessageId, DMessageMetadata } from './chat.message';
|
||||
|
||||
|
||||
/// Conversations Store
|
||||
|
||||
interface ChatState {
|
||||
conversations: DConversation[];
|
||||
}
|
||||
|
||||
export interface ChatActions {
|
||||
|
||||
// CRUD conversations
|
||||
prependNewConversation: (personaId: SystemPurposeId | undefined) => DConversationId;
|
||||
importConversation: (c: DConversation, preventClash: boolean) => DConversationId;
|
||||
branchConversation: (cId: DConversationId, mId: DMessageId | null) => DConversationId | null;
|
||||
deleteConversations: (cIds: DConversationId[], newConversationPersonaId?: SystemPurposeId) => DConversationId;
|
||||
|
||||
// within a conversation
|
||||
setAbortController: (cId: DConversationId, abortController: AbortController | null) => void;
|
||||
abortTyping: (cId: DConversationId) => void;
|
||||
setMessages: (cId: DConversationId, messages: DMessage[]) => void;
|
||||
appendMessage: (cId: DConversationId, message: DMessage) => void;
|
||||
deleteMessage: (cId: DConversationId, mId: DMessageId) => void;
|
||||
editMessage: (cId: DConversationId, mId: DMessageId, update: Partial<DMessage> | ((message: DMessage) => Partial<DMessage>), touchUpdated: boolean) => void;
|
||||
updateMetadata: (cId: DConversationId, mId: DMessageId, metadataDelta: Partial<DMessageMetadata>, touchUpdated?: boolean) => void;
|
||||
setSystemPurposeId: (cId: DConversationId, personaId: SystemPurposeId) => void;
|
||||
setAutoTitle: (cId: DConversationId, autoTitle: string) => void;
|
||||
setUserTitle: (cId: DConversationId, userTitle: string) => void;
|
||||
|
||||
// utility function
|
||||
_editConversation: (cId: DConversationId, update: Partial<DConversation> | ((conversation: DConversation) => Partial<DConversation>)) => void;
|
||||
}
|
||||
|
||||
type ConversationsStore = ChatState & ChatActions;
|
||||
|
||||
const defaultConversations: DConversation[] = [createDConversation()];
|
||||
|
||||
export const useChatStore = create<ConversationsStore>()(devtools(
|
||||
persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
// default state
|
||||
conversations: defaultConversations,
|
||||
|
||||
prependNewConversation: (personaId: SystemPurposeId | undefined): DConversationId => {
|
||||
const newConversation = createDConversation(personaId);
|
||||
|
||||
_set(state => ({
|
||||
conversations: [newConversation, ...state.conversations],
|
||||
}));
|
||||
|
||||
return newConversation.id;
|
||||
},
|
||||
|
||||
importConversation: (conversation: DConversation, preventClash: boolean): DConversationId => {
|
||||
const { conversations } = _get();
|
||||
|
||||
// if there's a clash, abort the former conversation, and optionally change the ID
|
||||
const existing = conversations.find(_c => _c.id === conversation.id);
|
||||
if (existing) {
|
||||
existing?.abortController?.abort();
|
||||
if (preventClash) {
|
||||
conversation.id = uuidv4();
|
||||
console.warn('Conversation ID clash, changing ID to', conversation.id);
|
||||
}
|
||||
}
|
||||
|
||||
conversation.tokenCount = updateMessagesTokenCounts(conversation.messages, true, 'importConversation');
|
||||
|
||||
_set({
|
||||
conversations: [conversation, ...conversations.filter(_c => _c.id !== conversation.id)],
|
||||
});
|
||||
|
||||
return conversation.id;
|
||||
},
|
||||
|
||||
branchConversation: (conversationId: DConversationId, messageId: DMessageId | null): DConversationId | null => {
|
||||
const { conversations } = _get();
|
||||
const conversation = conversations.find(_c => _c.id === conversationId);
|
||||
if (!conversation)
|
||||
return null;
|
||||
|
||||
const branched = duplicateCConversation(conversation, messageId ?? undefined);
|
||||
|
||||
_set({
|
||||
conversations: [branched, ...conversations],
|
||||
});
|
||||
|
||||
return branched.id;
|
||||
},
|
||||
|
||||
deleteConversations: (conversationIds: DConversationId[], newConversationPersonaId?: SystemPurposeId): DConversationId => {
|
||||
const { conversations } = _get();
|
||||
|
||||
// find the index of first conversation to delete
|
||||
const cIndex = conversationIds.length > 0 ? conversations.findIndex(_c => _c.id === conversationIds[0]) : -1;
|
||||
|
||||
// abort all pending requests
|
||||
conversationIds.forEach(conversationId => conversations.find(_c => _c.id === conversationId)?.abortController?.abort());
|
||||
|
||||
// remove from the list
|
||||
const newConversations = conversations.filter(_c => !conversationIds.includes(_c.id));
|
||||
|
||||
// create a new conversation if there are no more
|
||||
if (!newConversations.length)
|
||||
newConversations.push(createDConversation(newConversationPersonaId));
|
||||
|
||||
_set({
|
||||
conversations: newConversations,
|
||||
});
|
||||
|
||||
// return the next conversation Id in line, if valid
|
||||
return newConversations[(cIndex >= 0 && cIndex < newConversations.length) ? cIndex : 0].id;
|
||||
},
|
||||
|
||||
|
||||
// within a conversation
|
||||
|
||||
_editConversation: (conversationId: DConversationId, update: Partial<DConversation> | ((conversation: DConversation) => Partial<DConversation>)) =>
|
||||
_set(state => ({
|
||||
conversations: state.conversations.map((conversation): DConversation =>
|
||||
conversation.id === conversationId
|
||||
? {
|
||||
...conversation,
|
||||
...(typeof update === 'function' ? update(conversation) : update),
|
||||
}
|
||||
: conversation,
|
||||
),
|
||||
})),
|
||||
|
||||
setAbortController: (conversationId: DConversationId, abortController: AbortController | null) =>
|
||||
_get()._editConversation(conversationId, () =>
|
||||
({
|
||||
abortController: abortController,
|
||||
})),
|
||||
|
||||
abortTyping: (conversationId: DConversationId) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
conversation.abortController?.abort();
|
||||
return {
|
||||
abortController: null,
|
||||
};
|
||||
}),
|
||||
|
||||
setMessages: (conversationId: DConversationId, newMessages: DMessage[]) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
conversation.abortController?.abort();
|
||||
return {
|
||||
messages: newMessages,
|
||||
...(!!newMessages.length ? {} : {
|
||||
autoTitle: undefined,
|
||||
}),
|
||||
tokenCount: updateMessagesTokenCounts(newMessages, false, 'setMessages'),
|
||||
updated: Date.now(),
|
||||
abortController: null,
|
||||
};
|
||||
}),
|
||||
|
||||
appendMessage: (conversationId: DConversationId, message: DMessage) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
if (!message.typing)
|
||||
updateMessagesTokenCounts([message], true, 'appendMessage');
|
||||
|
||||
const messages = [...conversation.messages, message];
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: Date.now(),
|
||||
};
|
||||
}),
|
||||
|
||||
deleteMessage: (conversationId: DConversationId, messageId: DMessageId) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
const messages = conversation.messages.filter(message => message.id !== messageId);
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: Date.now(),
|
||||
};
|
||||
}),
|
||||
|
||||
editMessage: (conversationId: DConversationId, messageId: DMessageId, update: Partial<DMessage> | ((message: DMessage) => Partial<DMessage>), touchUpdated: boolean) =>
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
|
||||
const messages = conversation.messages.map((message): DMessage => {
|
||||
if (message.id !== messageId)
|
||||
return message;
|
||||
|
||||
const updatedMessage: DMessage = {
|
||||
...message,
|
||||
...(typeof update === 'function' ? update(message) : update),
|
||||
...(touchUpdated && { updated: Date.now() }),
|
||||
};
|
||||
|
||||
if (!updatedMessage.typing)
|
||||
updateMessageTokenCount(updatedMessage, getChatLLMId(), true, 'editMessage(typing=false)');
|
||||
|
||||
return updatedMessage;
|
||||
});
|
||||
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
}),
|
||||
|
||||
updateMetadata: (conversationId: DConversationId, messageId: DMessageId, metadataDelta: Partial<DMessageMetadata>, touchUpdated: boolean = true) => {
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
const messages = conversation.messages.map(message =>
|
||||
message.id !== messageId ? message
|
||||
: {
|
||||
...message,
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
...metadataDelta,
|
||||
},
|
||||
updated: touchUpdated ? Date.now() : message.updated,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
messages,
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSystemPurposeId: (conversationId: DConversationId, personaId: SystemPurposeId) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
systemPurposeId: personaId,
|
||||
}),
|
||||
|
||||
setAutoTitle: (conversationId: DConversationId, autoTitle: string) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
autoTitle,
|
||||
}),
|
||||
|
||||
setUserTitle: (conversationId: DConversationId, userTitle: string) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
userTitle,
|
||||
}),
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'app-chats',
|
||||
/* Version history:
|
||||
* - 1: [2023-03-18] App launch, single chat
|
||||
* - 2: [2023-04-10] Multi-chat version - invalidating data to be sure
|
||||
* - 3: [2023-09-19] Switch to IndexedDB - no data shape change,
|
||||
* but we swapped the backend (localStorage -> IndexedDB)
|
||||
* - 4: [2024-05-14] Convert messages to multi-part, removed the IDB migration
|
||||
*/
|
||||
version: 4,
|
||||
storage: createJSONStorage(() => idbStateStorage),
|
||||
|
||||
// Migrations
|
||||
migrate: (state: any, fromVersion: number): ConversationsStore => {
|
||||
|
||||
// 3 -> 4: Convert messages to multi-part
|
||||
if (fromVersion < 4 && state && state.conversations && state.conversations.length)
|
||||
state.conversations.forEach(convertCConversation_V3_V4);
|
||||
|
||||
return state;
|
||||
},
|
||||
|
||||
// Pre-Saving: remove transient properties
|
||||
partialize: (state) => ({
|
||||
...state,
|
||||
conversations: state.conversations.map((conversation: DConversation) => {
|
||||
const { abortController, ...rest } = conversation;
|
||||
return rest;
|
||||
}),
|
||||
}),
|
||||
|
||||
// Post-Loading: re-add transient properties and cleanup state
|
||||
onRehydrateStorage: () => (state) => {
|
||||
if (!state) return;
|
||||
|
||||
// fixup conversations
|
||||
for (const conversation of (state.conversations || [])) {
|
||||
// re-add transient properties
|
||||
conversation.abortController = null;
|
||||
// fixup messages
|
||||
for (const message of conversation.messages)
|
||||
message.typing = false;
|
||||
}
|
||||
},
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'AppChats',
|
||||
enabled: false,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
// Convenience function to update a set of messages, using the current chatLLM
|
||||
function updateMessagesTokenCounts(messages: DMessage[], forceUpdate: boolean, debugFrom: string): number {
|
||||
const chatLLMId = getChatLLMId();
|
||||
return 3 + messages.reduce((sum, message) => {
|
||||
return 4 + updateMessageTokenCount(message, chatLLMId, forceUpdate, debugFrom) + sum;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Convenience function to count the tokens in a DMessage object
|
||||
function updateMessageTokenCount(message: DMessage, llmId: DLLMId | null, forceUpdate: boolean, debugFrom: string): number {
|
||||
if (forceUpdate || !message.tokenCount) {
|
||||
if (!llmId) {
|
||||
message.tokenCount = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NOTE: temporary flattening of text-only parts, until we figure out a better way to handle this
|
||||
// FIXME: this is a quick and dirty hack, until we move token counting outside
|
||||
const messageTextParts = message.content.reduce((fullText, part) => fullText + (part.type === 'text' ? part.text : ''), '');
|
||||
// TODO: handle attachments too
|
||||
message.tokenCount = countModelTokens(messageTextParts, llmId, debugFrom) ?? 0;
|
||||
}
|
||||
return message.tokenCount;
|
||||
}
|
||||
|
||||
|
||||
export const getConversation = (conversationId: DConversationId | null): DConversation | null =>
|
||||
conversationId ? useChatStore.getState().conversations.find(_c => _c.id === conversationId) ?? null : null;
|
||||
|
||||
export const getConversationSystemPurposeId = (conversationId: DConversationId | null): SystemPurposeId | null =>
|
||||
getConversation(conversationId)?.systemPurposeId || null;
|
||||
|
||||
|
||||
export const useConversation = (conversationId: DConversationId | null) => useChatStore(useShallow(state => {
|
||||
const { conversations } = state;
|
||||
|
||||
// this object will change if any sub-prop changes as well
|
||||
const conversation = conversationId ? conversations.find(_c => _c.id === conversationId) ?? null : null;
|
||||
const title = conversation ? conversationTitle(conversation) : null;
|
||||
const isEmpty = conversation ? !conversation.messages.length : true;
|
||||
const isDeveloper = conversation?.systemPurposeId === 'Developer';
|
||||
const conversationIdx = conversation ? conversations.findIndex(_c => _c.id === conversation.id) : -1;
|
||||
|
||||
const hasConversations = conversations.length > 1 || (conversations.length === 1 && !!conversations[0].messages.length);
|
||||
const recycleNewConversationId = (conversations.length && !conversations[0].messages.length) ? conversations[0].id : null;
|
||||
|
||||
return {
|
||||
title,
|
||||
isEmpty,
|
||||
isDeveloper,
|
||||
conversationIdx,
|
||||
hasConversations,
|
||||
recycleNewConversationId,
|
||||
prependNewConversation: state.prependNewConversation,
|
||||
branchConversation: state.branchConversation,
|
||||
deleteConversations: state.deleteConversations,
|
||||
};
|
||||
}));
|
||||
@@ -1,7 +1,8 @@
|
||||
import { llmChatGenerateOrThrow, VChatFunctionIn } from '~/modules/llms/llm.client';
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { createTextPart, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
|
||||
|
||||
/*const suggestUserFollowUpFn: VChatFunctionIn = {
|
||||
@@ -67,7 +68,7 @@ export function autoSuggestions(conversationId: string, assistantMessageId: stri
|
||||
|
||||
// Execute the following follow-ups in parallel
|
||||
// const assistantMessageId = assistantMessage.id;
|
||||
let assistantMessageText = assistantMessage.text;
|
||||
let assistantMessageText = singleTextOrThrow(assistantMessage);
|
||||
|
||||
// Follow-up: Question
|
||||
if (suggestQuestions) {
|
||||
@@ -84,8 +85,8 @@ export function autoSuggestions(conversationId: string, assistantMessageId: stri
|
||||
// Follow-up: Auto-Diagrams
|
||||
if (suggestDiagrams) {
|
||||
void llmChatGenerateOrThrow(funcLLMId, [
|
||||
{ role: 'system', content: systemMessage.text },
|
||||
{ role: 'user', content: userMessage.text },
|
||||
{ role: 'system', content: singleTextOrThrow(systemMessage) },
|
||||
{ role: 'user', content: singleTextOrThrow(userMessage) },
|
||||
{ role: 'assistant', content: assistantMessageText },
|
||||
], [suggestPlantUMLFn], 'draw_plantuml_diagram',
|
||||
).then(chatResponse => {
|
||||
@@ -105,7 +106,7 @@ export function autoSuggestions(conversationId: string, assistantMessageId: stri
|
||||
|
||||
// append the PlantUML diagram to the assistant response
|
||||
editMessage(conversationId, assistantMessageId, {
|
||||
text: assistantMessageText + `\n\n\`\`\`${type}.diagram\n${plantUML}\n\`\`\`\n`,
|
||||
content: [createTextPart(assistantMessageText + `\n\n\`\`\`${type}.diagram\n${plantUML}\n\`\`\`\n`)],
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getFastLLMId } from '~/modules/llms/store-llms';
|
||||
import { llmChatGenerateOrThrow } from '~/modules/llms/llm.client';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
|
||||
/**
|
||||
@@ -26,7 +27,8 @@ export async function conversationAutoTitle(conversationId: string, forceReplace
|
||||
|
||||
// first line of the last 5 messages
|
||||
const historyLines: string[] = conversation.messages.filter(m => m.role !== 'system').slice(-5).map(m => {
|
||||
let text = m.text.split('\n')[0];
|
||||
const messageText = singleTextOrThrow(m);
|
||||
let text = messageText.split('\n')[0];
|
||||
text = text.length > 50 ? text.substring(0, 50) + '...' : text;
|
||||
text = `${m.role === 'user' ? 'You' : 'Assistant'}: ${text}`;
|
||||
return `- ${text}`;
|
||||
|
||||
@@ -15,7 +15,8 @@ import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { adjustContentScaling } from '~/common/app.theme';
|
||||
import { createDMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { createDMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useFormRadio } from '~/common/components/forms/useFormRadio';
|
||||
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
@@ -95,7 +96,8 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
const stepAbortController = new AbortController();
|
||||
setAbortController(stepAbortController);
|
||||
|
||||
const diagramPrompt = bigDiagramPrompt(diagramType, diagramLanguage, systemMessage.text, subject, customInstruction);
|
||||
const systemMessageText = singleTextOrThrow(systemMessage);
|
||||
const diagramPrompt = bigDiagramPrompt(diagramType, diagramLanguage, systemMessageText, subject, customInstruction);
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(diagramLlm.id, diagramPrompt, null, null, stepAbortController.signal,
|
||||
|
||||
@@ -7,9 +7,11 @@ import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import { useStreamChatText } from '~/modules/aifn/useStreamChatText';
|
||||
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { DMessage, createDMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { getConversation, useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
|
||||
import { FLATTEN_PROFILES, FlattenStyleType } from './flatten.data';
|
||||
@@ -68,7 +70,8 @@ function encodeConversationAsUserMessage(userPrompt: string, messages: DMessage[
|
||||
for (const message of messages) {
|
||||
if (message.role === 'system') continue;
|
||||
const author = message.role === 'user' ? 'User' : 'Assistant';
|
||||
const text = message.text.replace(/\n/g, '\n\n');
|
||||
const messageText = singleTextOrThrow(message);
|
||||
const text = messageText.replace(/\n/g, '\n\n');
|
||||
encodedMessages += `---${author}---\n${text}\n\n`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createDMessage, DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, createDMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
|
||||
const replyToSystemPrompt = `The user is referring to this in particular:
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import type { DMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { CUSTOM_FACTORY_ID, FFactoryId, findFusionFactory, FUSION_FACTORIES, FUSION_FACTORY_DEFAULT } from './instructions/beam.gather.factories';
|
||||
import { GATHER_PLACEHOLDER } from '../beam.config';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { streamAssistantMessage } from '../../../../apps/chat/editors/chat-strea
|
||||
import type { VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, createTextPart, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { BaseInstruction, ExecutionInputState } from './beam.gather.execution';
|
||||
@@ -48,17 +48,17 @@ export async function executeChatGenerate(_i: ChatGenerateInstruction, inputs: E
|
||||
// s0-h0-u0
|
||||
...inputs.chatMessages
|
||||
.filter((m) => (m.role === 'user' || m.role === 'assistant'))
|
||||
.map((m): VChatMessageIn => ({ role: (m.role !== 'assistant') ? 'user' : m.role, content: m.text })),
|
||||
.map((m): VChatMessageIn => ({ role: (m.role !== 'assistant') ? 'user' : m.role, content: singleTextOrThrow(m) })),
|
||||
// aN
|
||||
...inputs.rayMessages
|
||||
.map((m): VChatMessageIn => ({ role: 'assistant', content: m.text })),
|
||||
.map((m): VChatMessageIn => ({ role: 'assistant', content: singleTextOrThrow(m) })),
|
||||
// u
|
||||
{ role: 'user', content: _mixChatGeneratePrompt(_i.userPrompt, inputs.rayMessages.length, prevStepOutput) },
|
||||
];
|
||||
|
||||
// reset the intermediate message
|
||||
Object.assign(inputs.intermediateDMessage, {
|
||||
text: GATHER_PLACEHOLDER,
|
||||
content: [createTextPart(GATHER_PLACEHOLDER)],
|
||||
updated: undefined,
|
||||
} satisfies Partial<DMessage>);
|
||||
|
||||
@@ -66,8 +66,8 @@ export async function executeChatGenerate(_i: ChatGenerateInstruction, inputs: E
|
||||
const onMessageUpdate = (update: Partial<DMessage>) => {
|
||||
// in-place update of the intermediate message
|
||||
Object.assign(inputs.intermediateDMessage, update);
|
||||
if (update.text)
|
||||
inputs.intermediateDMessage.updated = Date.now();
|
||||
// if (update.text) // TODO: port to contents
|
||||
inputs.intermediateDMessage.updated = Date.now();
|
||||
|
||||
switch (_i.display) {
|
||||
case 'mute':
|
||||
@@ -75,7 +75,7 @@ export async function executeChatGenerate(_i: ChatGenerateInstruction, inputs: E
|
||||
|
||||
case 'character-count':
|
||||
inputs.updateInstructionComponent(
|
||||
<Typography level='body-xs' sx={{ opacity: 0.5 }}>{update.text?.length || 0} characters</Typography>,
|
||||
<Typography level='body-xs' sx={{ opacity: 0.5 }}>{singleTextOrThrow(update as any)?.length || 0} characters</Typography>,
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function executeChatGenerate(_i: ChatGenerateInstruction, inputs: E
|
||||
throw new Error(`Model execution error: ${status.errorMessage || 'Unknown error'}`);
|
||||
|
||||
// Proceed to the next step
|
||||
return inputs.intermediateDMessage.text;
|
||||
return singleTextOrThrow(inputs.intermediateDMessage);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Typography } from '@mui/joy';
|
||||
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import { createDMessage, type DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, createDMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import type { BFusion, FusionUpdateOrFn } from '../beam.gather';
|
||||
import { ChatGenerateInstruction, executeChatGenerate } from './ChatGenerateInstruction';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Box, Typography } from '@mui/joy';
|
||||
|
||||
import { ChatMessageMemo } from '../../../apps/chat/components/message/ChatMessage';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import type { DMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { BEAM_INVERT_BACKGROUND } from '../beam.config';
|
||||
import { useModuleBeamStore } from '../store-module-beam';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { streamAssistantMessage } from '../../../apps/chat/editors/chat-stream';
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
import type { VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
|
||||
import { createDMessage, DMessage } from '~/common/state/store-chats';
|
||||
import { createDMessage, DMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { RootStoreSlice } from '../store-beam-vanilla';
|
||||
@@ -228,9 +228,9 @@ export const createScatterSlice: StateCreator<RootStoreSlice & ScatterStoreSlice
|
||||
// Note: message.originLLM misss the prefix (e.g. gpt-4-0125 wihtout 'openai-..') so it won't match here
|
||||
const ray = createBRay(raysLlmId);
|
||||
// pre-fill the ray status with the message and to a successful state
|
||||
if (message.text.trim()) {
|
||||
if (singleTextOrThrow(message).trim()) {
|
||||
ray.status = 'success';
|
||||
ray.message.text = message.text;
|
||||
ray.message.content = [...message.content];
|
||||
ray.message.updated = Date.now();
|
||||
ray.imported = true;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createStore, StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import { DLLMId, getDiverseTopLlmIds } from '~/modules/llms/store-llms';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { BeamConfigSnapshot, useModuleBeamStore } from './store-module-beam';
|
||||
import { SCATTER_RAY_DEF } from './beam.config';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Box, Button, Tooltip, Typography } from '@mui/joy';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { ContentScaling, lineHeightChatTextMd, themeScalingMap } from '~/common/app.theme';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
|
||||
@@ -50,7 +50,7 @@ const renderBlocksSx: SxProps = {
|
||||
type BlocksRendererProps = {
|
||||
// required
|
||||
text: string;
|
||||
fromRole: DMessage['role'];
|
||||
fromRole: DMessageRole;
|
||||
contentScaling: ContentScaling;
|
||||
renderTextAsMarkdown: boolean;
|
||||
renderTextDiff?: TextDiff[];
|
||||
|
||||
@@ -81,7 +81,7 @@ type ItemDataOrigin = UploadOrigin | GeneratedOrigin;
|
||||
// Item Base type
|
||||
|
||||
interface DBlobBase<TType extends DBlobMetaDataType, TMime extends DBlobMimeType, TMeta extends Record<string, any>> {
|
||||
id: string; // Unique identifier
|
||||
id: DBlobId; // Unique identifier
|
||||
type: TType; // Type of item, used for discrimination
|
||||
|
||||
label: string; // Textual representation
|
||||
@@ -95,6 +95,8 @@ interface DBlobBase<TType extends DBlobMetaDataType, TMime extends DBlobMimeType
|
||||
cache: Record<string, DBlobData<DBlobMimeType>>; // Cached conversions as BlobData objects
|
||||
}
|
||||
|
||||
export type DBlobId = string;
|
||||
|
||||
export function createDBlobBase<TType extends DBlobMetaDataType, TMime extends DBlobMimeType, TMeta extends Record<string, any>>(type: TType, label: string, data: DBlobData<TMime>, origin: ItemDataOrigin, metadata: TMeta): DBlobBase<TType, TMime, TMeta> {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
|
||||
@@ -6,9 +6,10 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
|
||||
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
|
||||
|
||||
import { DConversationId, getConversation } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { getConversation } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import { ChatLinkExport } from './link/ChatLinkExport';
|
||||
import { PublishExport } from './publish/PublishExport';
|
||||
|
||||
@@ -4,13 +4,15 @@ import { Box, Button, FormControl, Input, Sheet, Textarea, Typography } from '@m
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { DConversationId, createDConversation } from '~/common/stores/chat/chat.conversation';
|
||||
import { DMessage, createDMessage } from '~/common/stores/chat/chat.message';
|
||||
import { FormRadioOption } from '~/common/components/forms/FormRadioControl';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { createDConversation, createDMessage, DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useFormRadio } from '~/common/components/forms/useFormRadio';
|
||||
|
||||
import type { ChatGptSharedChatSchema } from './server/chatgpt';
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { Alert, Box, Divider, IconButton, List, ListItem, Tooltip, Typography } from '@mui/joy';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
|
||||
import type { DConversation, DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversation, DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { DConversationId } from '~/common/state/store-chats';
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
|
||||
import { ExportChats, ExportConfig } from './ExportChats';
|
||||
|
||||
@@ -5,11 +5,12 @@ import DoneIcon from '@mui/icons-material/Done';
|
||||
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { DConversationId, conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { addSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { conversationTitle, DConversationId, getConversation } from '~/common/state/store-chats';
|
||||
import { getConversation } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import type { StoragePutSchema, StorageUpdateDeletionKeySchema } from '../server/link';
|
||||
import { ChatLinkDetails } from './ChatLinkDetails';
|
||||
|
||||
@@ -5,11 +5,12 @@ import ExitToAppIcon from '@mui/icons-material/ExitToApp';
|
||||
|
||||
import { getChatShowSystemMessages } from '../../../apps/chat/store-app-chat';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { DConversationId, getConversation } from '~/common/state/store-chats';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { getConversation } from '~/common/stores/chat/store-chats';
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
|
||||
import type { PublishedSchema } from '../server/pastegg';
|
||||
|
||||
@@ -5,11 +5,13 @@ import { defaultSystemPurposeId, SystemPurposeId, SystemPurposes } from '../../d
|
||||
import { DModelSource, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { DConversation, DConversationId, conversationTitle } from '~/common/stores/chat/chat.conversation';
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { DMessage, singleTextOrThrow } from '~/common/stores/chat/chat.message';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle, DConversation, type DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
|
||||
import { ImportedOutcome } from './ImportOutcomeModal';
|
||||
|
||||
@@ -205,7 +207,7 @@ export function conversationToMarkdown(conversation: DConversation, hideSystemMe
|
||||
: '';
|
||||
return mdTitle + conversation.messages.filter(message => !hideSystemMessage || message.role !== 'system').map(message => {
|
||||
let sender: string = message.sender;
|
||||
let text = message.text;
|
||||
let text = singleTextOrThrow(message);
|
||||
switch (message.role) {
|
||||
case 'system':
|
||||
sender = '✨ System message';
|
||||
@@ -215,7 +217,7 @@ export function conversationToMarkdown(conversation: DConversation, hideSystemMe
|
||||
const purpose = message.purposeId || conversation.systemPurposeId || null;
|
||||
sender = `${purpose || 'Assistant'} · *${prettyBaseModel(message.originLLM || '')}*`.trim();
|
||||
if (purpose && purpose in SystemPurposes)
|
||||
sender = `${SystemPurposes[purpose]?.symbol || ''} ${sender}`.trim();
|
||||
sender = `${SystemPurposes[purpose as SystemPurposeId]?.symbol || ''} ${sender}`.trim();
|
||||
break;
|
||||
case 'user':
|
||||
sender = '👤 You';
|
||||
|
||||
Reference in New Issue
Block a user