diff --git a/i18n/mobile/chat/en.json b/i18n/mobile/chat/en.json index bc19f26..452dd12 100644 --- a/i18n/mobile/chat/en.json +++ b/i18n/mobile/chat/en.json @@ -120,7 +120,8 @@ }, "AI_MESSAGE_ACTIONS": { "TEXT_EDIT": "Edit", - "TEXT_COPY": "Copy" + "TEXT_COPY": "Copy", + "TEXT_CONTINUE_RESPONSE": "Continue response" }, "EDIT_MESSAGE_INPUT": { "TEXT_EDIT_MESSAGE": "Edit message", diff --git a/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx b/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx index 8e94eea..110b0fe 100644 --- a/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx +++ b/libs/mobile/chat/features/ai-message-actions/src/lib/component.tsx @@ -12,12 +12,16 @@ import { ToastService } from '@open-webui-react-native/shared/utils/toast-servic interface AiMessageActionsProps { message: Message; onEditPress: (messageId: string, content: string) => void; + onContinueResponsePress: (messageId: string, content: string) => void; + isLast: boolean; } //TODO Extend with more actions - https://www.figma.com/design/YPCZjyVlD86psDwUxvMVBc/OpenWebUI-Redesign-React-Native?node-id=27540-25291&t=kg2yUIDp3UQDStLf-0 export function AiMessageActions({ message, onEditPress, + onContinueResponsePress, + isLast, children, }: PropsWithChildren): ReactElement { const translate = useTranslation('CHAT.AI_MESSAGE_ACTIONS'); @@ -35,6 +39,11 @@ export function AiMessageActions({ actionsSheetRef.current?.dismiss(); }; + const handleContinueResponsePress = (): void => { + onContinueResponsePress(message.id, message.content); + actionsSheetRef.current?.dismiss(); + }; + const actions: Array = compact([ isFeatureEnabled(FeatureID.AI_EDIT_MESSAGE) && { title: translate('TEXT_EDIT'), @@ -46,6 +55,11 @@ export function AiMessageActions({ iconName: 'copy', onPress: copyToClipboard, }, + isLast && { + title: translate('TEXT_CONTINUE_RESPONSE'), + iconName: 'play', + onPress: handleContinueResponsePress, + }, ]); return ( diff --git a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx index e84e2b0..fb94147 100644 --- a/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/components/messages-list/component.tsx @@ -1,4 +1,5 @@ import { FlashList } from '@shopify/flash-list'; +import { useLocalSearchParams } from 'expo-router'; import { delay } from 'lodash-es'; import { ReactElement, useCallback, useRef, useState } from 'react'; import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; @@ -6,9 +7,17 @@ import { useSharedValue, withTiming } from 'react-native-reanimated'; import { AiMessageActions } from '@open-webui-react-native/mobile/chat/features/ai-message-actions'; import { useManageMessageSiblings } from '@open-webui-react-native/mobile/chat/features/use-manage-messages-siblings'; import { UserMessageActions } from '@open-webui-react-native/mobile/chat/features/user-message-actions'; +import { useSetSelectedModel } from '@open-webui-react-native/mobile/shared/features/use-set-selected-model'; import { View, AppFlashList } from '@open-webui-react-native/mobile/shared/ui/ui-kit'; -import { History as ChatHistory, Message } from '@open-webui-react-native/shared/data-access/api'; +import { ChatScreenParams } from '@open-webui-react-native/mobile/shared/utils/navigation'; +import { + chatApi, + History as ChatHistory, + Message, + prepareCompleteChatPayload, +} from '@open-webui-react-native/shared/data-access/api'; import { Role } from '@open-webui-react-native/shared/data-access/common'; +import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; import { ChatAiMessage } from '../ai-message'; import { ChatBottomButton } from '../chat-bottom-button'; import { ChatUserMessage } from '../user-message'; @@ -18,9 +27,9 @@ interface ChatMessagesListProps { isMessagesListLoaded: boolean; onLayout: () => void; isInputFocusing: boolean; + onEditPress: (messageId: string, content: string) => void; history?: ChatHistory; messages?: Array; - onEditPress: (messageId: string, content: string) => void; editingMessageId?: string; } @@ -42,6 +51,9 @@ export default function ChatMessagesList({ const [autoscrollToBottomThreshold, setAutoscrollToBottomThreshold] = useState(1); const { showPreviousSibling, showNextSibling, getSiblingsInfo } = useManageMessageSiblings(chatId, history); + const { mutate: completeChat } = chatApi.useCompleteChat(); + const { id }: ChatScreenParams = useLocalSearchParams(); + const { modelId } = useSetSelectedModel(id); const handleContentSizeChange = (): void => { //NOTE: Needs to wait until the initial scroll to the bottom or content generation finished and not show the ChatBottomButton before @@ -116,13 +128,32 @@ export default function ChatMessagesList({ }, 1000); }; + const handleContinueResponsePress = (messageId: string): void => { + if (!modelId) return; + + const completePayload = prepareCompleteChatPayload({ + chatId, + messages, + messageId: messageId, + sessionId: socketService.socketSessionId, + model: modelId, + }); + completeChat(completePayload); + }; + const renderItem = useCallback( ({ item, index }: { item: Message; index: number }) => { const message = history?.messages[item.id]; if (!message) return null; + const isLast = item.id === history?.lastAssistantMessage?.id; + return item.role === Role.ASSISTANT ? ( - + handleEditPress(index, message.id, message.content)} @@ -144,7 +175,7 @@ export default function ChatMessagesList({ ); }, - [history, onEditPress, editingMessageId, showPreviousSibling, showNextSibling, getSiblingsInfo], + [history, onEditPress, editingMessageId, showPreviousSibling, showNextSibling, getSiblingsInfo, modelId], ); return ( diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts b/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts index 8269688..c243b7e 100644 --- a/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/index.ts @@ -36,6 +36,7 @@ import microphone from './microphone.svg'; import moreDots from './more-dots.svg'; import noWifi from './no-wifi.svg'; import pin from './pin.svg'; +import play from './play.svg'; import plusInCircle from './plus-in-circle.svg'; import plus from './plus.svg'; import search from './search.svg'; @@ -96,4 +97,5 @@ export const Icons = { strokeLeft, tick, stop, + play, }; diff --git a/libs/mobile/shared/ui/ui-kit/src/assets/icons/play.svg b/libs/mobile/shared/ui/ui-kit/src/assets/icons/play.svg new file mode 100644 index 0000000..8f8dec1 --- /dev/null +++ b/libs/mobile/shared/ui/ui-kit/src/assets/icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/shared/data-access/api/src/lib/chats/models/history.ts b/libs/shared/data-access/api/src/lib/chats/models/history.ts index 96ec53f..59a959a 100644 --- a/libs/shared/data-access/api/src/lib/chats/models/history.ts +++ b/libs/shared/data-access/api/src/lib/chats/models/history.ts @@ -13,4 +13,14 @@ export class History { constructor(history: Partial = {}) { Object.assign(this, history); } + + public get lastAssistantMessage(): Message | undefined { + if (!this.messages) return undefined; + + const assistantMessages = Object.values(this.messages).filter((m) => m.role === 'assistant'); + + if (assistantMessages.length === 0) return undefined; + + return assistantMessages.sort((a, b) => b.timestamp - a.timestamp)[0]; + } } diff --git a/libs/shared/data-access/api/src/lib/chats/utils/patch-chat-message-with-completion.ts b/libs/shared/data-access/api/src/lib/chats/utils/patch-chat-message-with-completion.ts index f3cea45..e361563 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/patch-chat-message-with-completion.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/patch-chat-message-with-completion.ts @@ -41,6 +41,7 @@ export function patchChatMessagesWithCompletion( ? { ...history, messages: updatedHistoryMessages, + lastAssistantMessage: history?.lastAssistantMessage ?? updatedLastMessage, } : history; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/patch-completed-message.ts b/libs/shared/data-access/api/src/lib/chats/utils/patch-completed-message.ts index e9c5fdd..b2cdb4d 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/patch-completed-message.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/patch-completed-message.ts @@ -36,6 +36,7 @@ export function patchCompletedMessage(oldData: ChatResponse | undefined): ChatRe ? { ...history, messages: updatedHistoryMessages, + lastAssistantMessage: history?.lastAssistantMessage ?? updatedLastMessage, } : history; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts index 71f804e..ace2908 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-update-message-in-chat-payload.ts @@ -17,6 +17,8 @@ export function prepareUpdateMessageInChatPayload( msg.id === messageId ? { ...messagesMap[messageId] } : msg, ); + const lastAssistantMessage = oldData.chat.history.lastAssistantMessage; + const newChatData: ChatResponse = { ...oldData, chat: { @@ -24,6 +26,7 @@ export function prepareUpdateMessageInChatPayload( history: { ...oldData.chat.history, messages: messagesMap, + lastAssistantMessage, }, messages: messagesArray, },