From 78bb84bd52f9c06d113f78ea77462fcb7da7e6f3 Mon Sep 17 00:00:00 2001 From: tfomkin Date: Thu, 18 Dec 2025 21:56:05 +0700 Subject: [PATCH 1/2] feat: regenerate AI message functionality --- .../ai-message-actions/src/lib/component.tsx | 41 +++++++++-- .../chat/features/chat/src/lib/component.tsx | 24 ++++++- .../components/messages-list/component.tsx | 12 ++++ .../src/use-suggest-change.ts | 69 +++++++++++++++++-- .../utils/create-regeneration-messages.ts | 25 +++++++ .../api/src/lib/chats/utils/index.ts | 2 + .../chats/utils/prepare-regenerate-payload.ts | 30 ++++++++ 7 files changed, 191 insertions(+), 12 deletions(-) create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts create mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts 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 17b9975..a7a00ff 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 @@ -15,6 +15,9 @@ interface AiMessageActionsProps { onEditPress: (messageId: string, content: string) => void; onSuggestPress: (messageId: string) => void; onContinueResponsePress: (messageId: string, content: string) => void; + onTryAgain: (messageId: string) => void; + onAddDetails: (messageId: string) => void; + onMoreConcise: (messageId: string) => void; isLast: boolean; } @@ -24,6 +27,9 @@ export function AiMessageActions({ onEditPress, onSuggestPress, onContinueResponsePress, + onTryAgain, + onAddDetails, + onMoreConcise, isLast, children, }: PropsWithChildren): ReactElement { @@ -43,22 +49,40 @@ export function AiMessageActions({ actionsSheetRef.current?.dismiss(); }; - const handleSuggestPress = (): void => { + const handleContinueResponsePress = (): void => { + onContinueResponsePress(message.id, message.content); actionsSheetRef.current?.dismiss(); + }; + + const openRegenerateActions = (): void => { + regenerateActionsSheetRef.current?.present(); + }; + + const runRegenerateAction = (action?: (messageId: string) => void): void => { + actionsSheetRef.current?.dismiss(); + //NOTE: Small delay ensures sheet is fully closed before showing input setTimeout(() => { - onSuggestPress(message.id); + action?.(message.id); }, 100); + regenerateActionsSheetRef.current?.dismiss(); }; - const handleContinueResponsePress = (): void => { - onContinueResponsePress(message.id, message.content); - actionsSheetRef.current?.dismiss(); + const handleSuggestPress = (): void => { + runRegenerateAction(onSuggestPress); }; - const openRegenerateActions = (): void => { - regenerateActionsSheetRef.current?.present(); + const handleTryAgainPress = (): void => { + runRegenerateAction(onTryAgain); + }; + + const handleAddDetailsPress = (): void => { + runRegenerateAction(onAddDetails); + }; + + const handleMoreConcisePress = (): void => { + runRegenerateAction(onMoreConcise); }; const actions: Array = compact([ @@ -94,14 +118,17 @@ export function AiMessageActions({ { title: translate('REGENERATE_MESSAGE_ACTION_SHEET.TEXT_TRY_AGAIN'), iconName: 'refresh', + onPress: handleTryAgainPress, }, { title: translate('REGENERATE_MESSAGE_ACTION_SHEET.TEXT_ADD_DETAILS'), iconName: 'moreText', + onPress: handleAddDetailsPress, }, { title: translate('REGENERATE_MESSAGE_ACTION_SHEET.TEXT_MORE_CONCISE'), iconName: 'lessText', + onPress: handleMoreConcisePress, }, ]); diff --git a/libs/mobile/chat/features/chat/src/lib/component.tsx b/libs/mobile/chat/features/chat/src/lib/component.tsx index 117260e..2958452 100644 --- a/libs/mobile/chat/features/chat/src/lib/component.tsx +++ b/libs/mobile/chat/features/chat/src/lib/component.tsx @@ -35,6 +35,7 @@ interface ChatProps { export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: ChatProps): ReactElement { const translate = useTranslation('CHAT.CHAT'); + const translateRegeneratePrompt = useTranslation('CHAT.AI_MESSAGE_ACTIONS.REGENERATE_MESSAGE_ACTION_SHEET'); const [isInputFocusing, setIsInputFocusing] = useState(false); //NOTE: Needs to avoid ChatBottomButton jumping when auto-scrolling after focus @@ -71,7 +72,8 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C cancelSuggesting, control: suggestMessageControl, submitSuggestion, - } = useSuggestChange(); + regenerateWithSuggestion, + } = useSuggestChange({ chat, modelId: selectedModelId }); const history = chat?.chat.history; const isResponseGenerating = !history?.messages[history.currentId].done; @@ -136,6 +138,23 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C setActiveInputMode(null); }; + const handleQuickSuggestion = (messageId: string, message: string): void => { + // NOTE: Quick suggestions should not open the suggest input, they should immediately trigger regeneration + void regenerateWithSuggestion(messageId, message); + }; + + const handleTryAgain = (messageId: string): void => { + handleQuickSuggestion(messageId, ''); + }; + + const handleAddDetails = (messageId: string): void => { + handleQuickSuggestion(messageId, translateRegeneratePrompt('TEXT_ADD_DETAILS')); + }; + + const handleMoreConcise = (messageId: string): void => { + handleQuickSuggestion(messageId, translateRegeneratePrompt('TEXT_MORE_CONCISE')); + }; + const onSubmit = (options: Array): Promise => handleSubmit(({ inputValue }: FormValues): void => { if (!selectedModelId) { @@ -175,6 +194,9 @@ export function Chat({ chatId, selectedModelId, isNewChat, resetToChatsList }: C void; onSuggestPress: (messageId: string) => void; + onTryAgain: (messageId: string) => void; + onAddDetails: (messageId: string) => void; + onMoreConcise: (messageId: string) => void; history?: ChatHistory; messages?: Array; editingMessageId?: string; @@ -45,6 +48,9 @@ export default function ChatMessagesList({ isInputFocusing, onEditPress, onSuggestPress, + onTryAgain, + onAddDetails, + onMoreConcise, editingMessageId, }: ChatMessagesListProps): ReactElement { const listRef = useRef>(null); @@ -174,6 +180,9 @@ export default function ChatMessagesList({ onEditPress={onEditPress} onSuggestPress={onSuggestPress} onContinueResponsePress={handleContinueResponsePress} + onTryAgain={onTryAgain} + onAddDetails={onAddDetails} + onMoreConcise={onMoreConcise} isLast={isLast}> >; handleSubmit: UseFormHandleSubmit>; startSuggesting: (messageId: string) => void; cancelSuggesting: () => void; - submitSuggestion: () => Promise; + submitSuggestion: (message: string) => Promise; + regenerateWithSuggestion: (messageId: string, message: string) => Promise; } -export const useSuggestChange = (): UseSuggestChangeReturn => { +export const useSuggestChange = ({ chat, modelId }: UseSuggestChangeProps): UseSuggestChangeReturn => { const [suggestingMessageId, setSuggestingMessageId] = useState(); const { control, handleSubmit, reset } = useForm>({ defaultValues: { suggestionInputValue: '' }, }); + const { mutate: completeChat } = chatApi.useCompleteChat(); + const startSuggesting = (messageId: string): void => { setSuggestingMessageId(messageId); reset({ suggestionInputValue: '' }); @@ -32,11 +51,52 @@ export const useSuggestChange = (): UseSuggestChangeReturn => { reset({ suggestionInputValue: '' }); }; - const submitSuggestion = async (): Promise => { - //TODO: implement suggestion sending logic + const regenerateForMessage = async (messageId: string, message: string): Promise => { + if (!chat?.chat || !modelId) return; + + const history = chat.chat.history; + const baseAssistant = history.messages[messageId]; + const parentUserId = baseAssistant.parentId!; + const newAssistantId = uuid.v4(); + const now = dayjs(); + const timestampSec = Math.floor(now.unix()); + + history.messages[newAssistantId] = new Message({ + id: newAssistantId, + timestamp: timestampSec, + parentId: parentUserId, + role: Role.ASSISTANT, + content: '', + model: modelId, + modelName: modelId, + childrenIds: [], + }); + + history.messages[parentUserId].childrenIds?.push(newAssistantId); + history.currentId = newAssistantId; + + const newMessagesList = createMessagesList(history, newAssistantId); + patchChatWithSelectedMessages(chat.id, newAssistantId, newMessagesList); + + const payload = prepareRegeneratePayload({ + chat, + messageId: newAssistantId, + model: modelId, + suggestionPrompt: message, + }); + + completeChat(payload); cancelSuggesting(); }; + const submitSuggestion = async (message: string): Promise => { + if (!suggestingMessageId) return; + await regenerateForMessage(suggestingMessageId, message); + }; + + const regenerateWithSuggestion = async (messageId: string, message: string): Promise => + regenerateForMessage(messageId, message); + return { suggestingMessageId, control, @@ -44,5 +104,6 @@ export const useSuggestChange = (): UseSuggestChangeReturn => { startSuggesting, cancelSuggesting, submitSuggestion, + regenerateWithSuggestion, }; }; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts b/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts new file mode 100644 index 0000000..202d66f --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts @@ -0,0 +1,25 @@ +import { History, Message } from '../models'; +import { createMessagesList } from './create-messages-list'; + +export function createRegenerationMessages( + history: History, + userMessageId: string, + suggestionPrompt?: string, +): Array { + const base = createMessagesList(history, userMessageId); + + if (!suggestionPrompt) { + return base; + } + + return base.map((msg, idx) => { + if (idx === base.length - 1 && msg.role === 'user') { + return { + ...msg, + content: `${msg.content}\n\n${suggestionPrompt}`, + }; + } + + return msg; + }); +} diff --git a/libs/shared/data-access/api/src/lib/chats/utils/index.ts b/libs/shared/data-access/api/src/lib/chats/utils/index.ts index 8cb9013..39ef219 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/index.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/index.ts @@ -18,3 +18,5 @@ export * from './prepare-update-message-in-chat-payload'; export * from './prepare-update-message-to-send-payload'; export * from './prepare-edit-assistant-message-payload'; export * from './prepare-copy-edited-message-payload'; +export * from './prepare-regenerate-payload'; +export * from './create-regeneration-messages'; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts new file mode 100644 index 0000000..9d23738 --- /dev/null +++ b/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts @@ -0,0 +1,30 @@ +import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; +import { ChatGenerationOption } from '../enums'; +import { ChatResponse, CompleteChatRequest } from '../models'; +import { createRegenerationMessages } from './create-regeneration-messages'; +import { prepareCompleteChatPayload } from './prepare-complete-chat-payload'; + +export function prepareRegeneratePayload({ + chat, + messageId, + model, + suggestionPrompt, + generationOptions, +}: { + chat: ChatResponse; + messageId: string; + model: string; + suggestionPrompt?: string; + generationOptions?: Array; +}): CompleteChatRequest { + const history = chat.chat.history; + + return prepareCompleteChatPayload({ + chatId: chat.id, + messageId, + messages: createRegenerationMessages(history, messageId, suggestionPrompt), + sessionId: socketService.socketSessionId, + model, + generationOptions, + }); +} From cfbcbef1ca523953cfb2fab54fd35faaa30d867c Mon Sep 17 00:00:00 2001 From: tfomkin Date: Tue, 23 Dec 2025 18:39:24 +0700 Subject: [PATCH 2/2] fix: regeneration messages logic simplified --- .../src/use-suggest-change.ts | 19 +++++++++--- .../utils/create-regeneration-messages.ts | 25 ---------------- .../api/src/lib/chats/utils/index.ts | 2 -- .../chats/utils/prepare-regenerate-payload.ts | 30 ------------------- 4 files changed, 15 insertions(+), 61 deletions(-) delete mode 100644 libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts delete mode 100644 libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts diff --git a/libs/mobile/chat/features/use-suggest-change/src/use-suggest-change.ts b/libs/mobile/chat/features/use-suggest-change/src/use-suggest-change.ts index 6cbaf06..b10f8fc 100644 --- a/libs/mobile/chat/features/use-suggest-change/src/use-suggest-change.ts +++ b/libs/mobile/chat/features/use-suggest-change/src/use-suggest-change.ts @@ -7,11 +7,12 @@ import { chatApi, ChatResponse, Message, - prepareRegeneratePayload, createMessagesList, patchChatWithSelectedMessages, + 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'; export interface SuggestChangeSchema { suggestionInputValue: string; @@ -61,6 +62,7 @@ export const useSuggestChange = ({ chat, modelId }: UseSuggestChangeProps): UseS const now = dayjs(); const timestampSec = Math.floor(now.unix()); + // create a new empty assistant message history.messages[newAssistantId] = new Message({ id: newAssistantId, timestamp: timestampSec, @@ -78,11 +80,20 @@ export const useSuggestChange = ({ chat, modelId }: UseSuggestChangeProps): UseS const newMessagesList = createMessagesList(history, newAssistantId); patchChatWithSelectedMessages(chat.id, newAssistantId, newMessagesList); - const payload = prepareRegeneratePayload({ - chat, + const regenerationMessages = [ + ...createMessagesList(history, messageId), + new Message({ + role: Role.USER, + content: message, + }), + ]; + + const payload = prepareCompleteChatPayload({ + chatId: chat.id, messageId: newAssistantId, + messages: regenerationMessages, + sessionId: socketService.socketSessionId, model: modelId, - suggestionPrompt: message, }); completeChat(payload); diff --git a/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts b/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts deleted file mode 100644 index 202d66f..0000000 --- a/libs/shared/data-access/api/src/lib/chats/utils/create-regeneration-messages.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { History, Message } from '../models'; -import { createMessagesList } from './create-messages-list'; - -export function createRegenerationMessages( - history: History, - userMessageId: string, - suggestionPrompt?: string, -): Array { - const base = createMessagesList(history, userMessageId); - - if (!suggestionPrompt) { - return base; - } - - return base.map((msg, idx) => { - if (idx === base.length - 1 && msg.role === 'user') { - return { - ...msg, - content: `${msg.content}\n\n${suggestionPrompt}`, - }; - } - - return msg; - }); -} diff --git a/libs/shared/data-access/api/src/lib/chats/utils/index.ts b/libs/shared/data-access/api/src/lib/chats/utils/index.ts index 39ef219..8cb9013 100644 --- a/libs/shared/data-access/api/src/lib/chats/utils/index.ts +++ b/libs/shared/data-access/api/src/lib/chats/utils/index.ts @@ -18,5 +18,3 @@ export * from './prepare-update-message-in-chat-payload'; export * from './prepare-update-message-to-send-payload'; export * from './prepare-edit-assistant-message-payload'; export * from './prepare-copy-edited-message-payload'; -export * from './prepare-regenerate-payload'; -export * from './create-regeneration-messages'; diff --git a/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts b/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts deleted file mode 100644 index 9d23738..0000000 --- a/libs/shared/data-access/api/src/lib/chats/utils/prepare-regenerate-payload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { socketService } from '@open-webui-react-native/shared/data-access/websocket'; -import { ChatGenerationOption } from '../enums'; -import { ChatResponse, CompleteChatRequest } from '../models'; -import { createRegenerationMessages } from './create-regeneration-messages'; -import { prepareCompleteChatPayload } from './prepare-complete-chat-payload'; - -export function prepareRegeneratePayload({ - chat, - messageId, - model, - suggestionPrompt, - generationOptions, -}: { - chat: ChatResponse; - messageId: string; - model: string; - suggestionPrompt?: string; - generationOptions?: Array; -}): CompleteChatRequest { - const history = chat.chat.history; - - return prepareCompleteChatPayload({ - chatId: chat.id, - messageId, - messages: createRegenerationMessages(history, messageId, suggestionPrompt), - sessionId: socketService.socketSessionId, - model, - generationOptions, - }); -}