diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 74324bc27..353c47a8b 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -31,7 +31,7 @@ import { createDMessageFromFragments, createDMessagePlaceholderIncomplete, DMess import { createErrorContentFragment, createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments'; import { gcChatImageAssets } from '~/common/stores/chat/chat.gc'; import { getChatLLMId } from '~/common/stores/llms/store-llms'; -import { getConversation, getConversationSystemPurposeId, isValidConversation, useConversation } from '~/common/stores/chat/store-chats'; +import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/store-chats'; import { optimaActions, optimaOpenModels, optimaOpenPreferences, useSetOptimaAppMenu } from '~/common/layout/optima/useOptima'; import { themeBgAppChatComposer } from '~/common/app.theme'; import { useChatLLM } from '~/common/stores/llms/llms.hooks'; @@ -254,13 +254,20 @@ export function AppChat() { }, [handleExecuteAndOutcome]); const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => { - const focusedConversation = getConversation(focusedPaneConversationId); - if (focusedPaneConversationId && focusedConversation?.messages?.length) { - const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1]; - if (lastMessage.role === 'assistant') - ConversationsManager.getHandler(focusedPaneConversationId).historyTruncateTo(lastMessage.id, -1); - await handleExecuteAndOutcome('generate-content', focusedConversation.id, 'chat-regenerate-last'); // truncate if assistant, then gen-text - } + // Ctrl + Shift + Z + if (!focusedPaneConversationId) return; + const cHandler = ConversationsManager.getHandler(focusedPaneConversationId); + if (!cHandler.isValid()) return; + const inputHistory = cHandler.historyViewHeadOrThrow('chat-regenerate-shortcut'); + if (!inputHistory.length) return; + + // remove the last message if assistant's + const lastMessage = inputHistory[inputHistory.length - 1]; + if (lastMessage.role === 'assistant') + cHandler.historyTruncateTo(lastMessage.id, -1); + + // generate: NOTE: this will replace the system message correctly + await handleExecuteAndOutcome('generate-content', focusedPaneConversationId, 'chat-regenerate-last'); // truncate if assistant, then gen-text }, [focusedPaneConversationId, handleExecuteAndOutcome]); const handleMessageBeamLastInFocusedPane = React.useCallback(async () => { @@ -284,10 +291,8 @@ export function AppChat() { const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []); const handleImagineFromText = React.useCallback(async (conversationId: DConversationId, subjectText: string) => { - const conversation = getConversation(conversationId); - if (!conversation) - return; const cHandler = ConversationsManager.getHandler(conversationId); + if (!cHandler.isValid()) return; const userImagineMessage = createDMessagePlaceholderIncomplete('user', `Thinking at the subject...`); // [chat] append user:imagine prompt cHandler.messageAppend(userImagineMessage); await imaginePromptFromTextOrThrow(subjectText, conversationId) diff --git a/src/apps/chat/components/composer/Composer.tsx b/src/apps/chat/components/composer/Composer.tsx index cf270b64c..e087b07dc 100644 --- a/src/apps/chat/components/composer/Composer.tsx +++ b/src/apps/chat/components/composer/Composer.tsx @@ -31,11 +31,11 @@ import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/compon import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore'; import { animationEnterBelow } from '~/common/util/animUtils'; import { browserSpeechRecognitionCapability, PLACEHOLDER_INTERIM_TRANSCRIPT, SpeechResult, useSpeechRecognition } from '~/common/components/speechrecognition/useSpeechRecognition'; -import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation'; +import { DConversationId } from '~/common/stores/chat/chat.conversation'; import { copyToClipboard, supportsClipboardRead } from '~/common/util/clipboardUtils'; import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments'; import { estimateTextTokens, glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens'; -import { getConversation, isValidConversation, useChatStore } from '~/common/stores/chat/store-chats'; +import { isValidConversation, useChatStore } from '~/common/stores/chat/store-chats'; import { getModelParameterValueOrThrow } from '~/common/stores/llms/llms.parameters'; import { launchAppCall, removeQueryParam, useRouterQuery } from '~/common/app.routes'; import { lineHeightTextareaMd } from '~/common/app.theme'; @@ -488,12 +488,12 @@ export function Composer(props: { const onActileEmbedMessage = React.useCallback(async ({ conversationId, messageId }: StarredMessageItem) => { // get the message - const conversation = getConversation(conversationId); - const messageToEmbed = conversation?.messages.find(m => m.id === messageId); - if (conversation && messageToEmbed) { + const cHandler = ConversationsManager.getHandler(conversationId); + const messageToEmbed = cHandler.historyFindMessageOrThrow(messageId); + if (messageToEmbed) { const fragmentsCopy = duplicateDMessageFragmentsNoVoid(messageToEmbed.fragments); // [attach] deep copy a message's fragments to attach to ego if (fragmentsCopy.length) { - const chatTitle = conversationTitle(conversation); + const chatTitle = cHandler.title() ?? ''; const messageText = messageFragmentsReduceText(fragmentsCopy); const label = `${chatTitle} > ${messageText.slice(0, 10)}...`; await attachAppendEgoFragments(fragmentsCopy, label, chatTitle, conversationId, messageId); diff --git a/src/common/chat-overlay/ConversationHandler.ts b/src/common/chat-overlay/ConversationHandler.ts index acbbb1a53..9b763126e 100644 --- a/src/common/chat-overlay/ConversationHandler.ts +++ b/src/common/chat-overlay/ConversationHandler.ts @@ -216,6 +216,14 @@ export class ConversationHandler { return messages; } + historyFindMessageOrThrow(messageId: DMessageId): Readonly | undefined { + return _chatStoreActions.historyView(this.conversationId)?.find(m => m.id === messageId); + } + + title(): string | undefined { + return _chatStoreActions.title(this.conversationId); + } + // Beam diff --git a/src/common/stores/chat/store-chats.ts b/src/common/stores/chat/store-chats.ts index 0ea827cc6..bca6b8194 100644 --- a/src/common/stores/chat/store-chats.ts +++ b/src/common/stores/chat/store-chats.ts @@ -53,6 +53,7 @@ export interface ChatActions { setAutoTitle: (cId: DConversationId, autoTitle: string) => void; setUserTitle: (cId: DConversationId, userTitle: string) => void; setUserSymbol: (cId: DConversationId, userSymbol: string | null) => void; + title: (cId: DConversationId) => string | undefined; // utility function _editConversation: (cId: DConversationId, update: Partial | ((conversation: DConversation) => Partial)) => void; @@ -389,6 +390,11 @@ export const useChatStore = create()(/*devtools(*/ ...(!userTitle && { autoTitle: undefined }), // clear autotitle when clearing usertitle }), + title: (conversationId: DConversationId): string | undefined => { + const existing = _get().conversations.find(_c => _c.id === conversationId); + return existing ? conversationTitle(existing) : undefined; + }, + setUserSymbol: (conversationId: DConversationId, userSymbol: string | null) => _get()._editConversation(conversationId, { diff --git a/src/modules/aifn/flatten/FlattenerModal.tsx b/src/modules/aifn/flatten/FlattenerModal.tsx index 799705589..6ec67f724 100644 --- a/src/modules/aifn/flatten/FlattenerModal.tsx +++ b/src/modules/aifn/flatten/FlattenerModal.tsx @@ -12,7 +12,6 @@ import { DConversationId } from '~/common/stores/chat/chat.conversation'; import { GoodModal } from '~/common/components/modals/GoodModal'; import { InlineTextarea } from '~/common/components/InlineTextarea'; import { createDMessageTextContent, DMessage, messageFragmentsReduceText } from '~/common/stores/chat/chat.message'; -import { getConversation } from '~/common/stores/chat/store-chats'; import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType'; import { FLATTEN_PROFILES, FlattenStyleType } from './flatten.data'; @@ -65,7 +64,7 @@ function FlatteningProgress(props: { llmLabel: string, partialText: string | nul } -function encodeConversationAsUserMessage(userPrompt: string, messages: DMessage[]): string { +function encodeConversationAsUserMessage(userPrompt: string, messages: Readonly): string { let encodedMessages = ''; for (const message of messages) { @@ -103,9 +102,11 @@ export function FlattenerModal(props: { const handlePerformFlattening = React.useCallback(async (flattenStyle: FlattenStyleType) => { // validate config (or set error) - const conversation = getConversation(props.conversationId); - const messages = conversation?.messages; - if (!messages || !messages.length) + if (!props.conversationId) + return setErrorMessage('No conversation selected'); + const cHandler = ConversationsManager.getHandler(props.conversationId); + const messages = !cHandler.isValid() ? [] : cHandler.historyViewHeadOrThrow('flattener-modal'); + if (!messages.length) return setErrorMessage('No messages in conversation'); if (!llm) return setErrorMessage('No model selected');