diff --git a/browser/ai_chat/android/ai_chat_utils.cc b/browser/ai_chat/android/ai_chat_utils.cc index 900fd0c5f9dc..1444509fc1e1 100644 --- a/browser/ai_chat/android/ai_chat_utils.cc +++ b/browser/ai_chat/android/ai_chat_utils.cc @@ -32,7 +32,7 @@ static void JNI_BraveLeoUtils_OpenLeoQuery( mojom::CharacterType::HUMAN, mojom::ActionType::QUERY, mojom::ConversationTurnVisibility::VISIBLE, base::android::ConvertJavaStringToUTF8(query), std::nullopt, std::nullopt, - base::Time::Now(), std::vector{}); + base::Time::Now(), std::nullopt); chat_tab_helper->SubmitHumanConversationEntry(std::move(turn)); #endif } diff --git a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc index be85d7b19ea1..660404fe64fa 100644 --- a/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc +++ b/browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc @@ -143,7 +143,7 @@ void AIChatUIPageHandler::SubmitHumanConversationEntry( mojom::ConversationTurnPtr turn = mojom::ConversationTurn::New( CharacterType::HUMAN, mojom::ActionType::UNSPECIFIED, ConversationTurnVisibility::VISIBLE, input, std::nullopt, std::nullopt, - base::Time::Now(), std::vector{}); + base::Time::Now(), std::nullopt); active_chat_tab_helper_->SubmitHumanConversationEntry(std::move(turn)); } diff --git a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc index e2d9ee905b30..e86fe930830d 100644 --- a/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc +++ b/chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc @@ -72,12 +72,13 @@ void ChromeAutocompleteProviderClient::OpenLeo(const std::u16string& query) { // Send the query to the AIChat's backend. ai_chat::mojom::ConversationTurnPtr turn = - ai_chat::mojom::ConversationTurn::New(); - turn->character_type = ai_chat::mojom::CharacterType::HUMAN; - turn->action_type = ai_chat::mojom::ActionType::QUERY; - turn->visibility = ai_chat::mojom::ConversationTurnVisibility::VISIBLE; - turn->text = base::UTF16ToUTF8(query); - turn->selected_text = std::nullopt; + ai_chat::mojom::ConversationTurn::New( + ai_chat::mojom::CharacterType::HUMAN, + ai_chat::mojom::ActionType::QUERY, + ai_chat::mojom::ConversationTurnVisibility::VISIBLE, + base::UTF16ToUTF8(query) /* text */, std::nullopt /* selected_text */, + std::nullopt /* events */, base::Time::Now(), + std::nullopt /* edits */); chat_tab_helper->SubmitHumanConversationEntry(std::move(turn)); diff --git a/components/ai_chat/core/browser/conversation_driver.cc b/components/ai_chat/core/browser/conversation_driver.cc index d5cf8d30308f..98bde71fd682 100644 --- a/components/ai_chat/core/browser/conversation_driver.cc +++ b/components/ai_chat/core/browser/conversation_driver.cc @@ -418,7 +418,7 @@ void ConversationDriver::UpdateOrCreateLastAssistantEntry( CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, ConversationTurnVisibility::VISIBLE, "", std::nullopt, std::vector{}, base::Time::Now(), - std::vector{}); + std::nullopt); chat_history_.push_back(std::move(entry)); } @@ -870,8 +870,7 @@ void ConversationDriver::AddSubmitSelectedTextError( const std::string& question = GetActionTypeQuestion(action_type); mojom::ConversationTurnPtr turn = mojom::ConversationTurn::New( CharacterType::HUMAN, action_type, ConversationTurnVisibility::VISIBLE, - question, selected_text, std::nullopt, base::Time::Now(), - std::vector{}); + question, selected_text, std::nullopt, base::Time::Now(), std::nullopt); AddToConversationHistory(std::move(turn)); SetAPIError(error); } @@ -928,8 +927,7 @@ void ConversationDriver::SubmitSelectedTextWithQuestion( // Use sidebar. mojom::ConversationTurnPtr turn = mojom::ConversationTurn::New( CharacterType::HUMAN, action_type, ConversationTurnVisibility::VISIBLE, - question, selected_text, std::nullopt, base::Time::Now(), - std::vector{}); + question, selected_text, std::nullopt, base::Time::Now(), std::nullopt); SubmitHumanConversationEntry(std::move(turn)); } else { @@ -941,6 +939,12 @@ void ConversationDriver::SubmitHumanConversationEntry( mojom::ConversationTurnPtr turn) { VLOG(1) << __func__; DVLOG(4) << __func__ << ": " << turn->text; + + // If there's edits, use the last one as the latest turn. + bool has_edits = turn->edits && !turn->edits->empty(); + mojom::ConversationTurnPtr& latest_turn = + has_edits ? turn->edits->back() : turn; + // Decide if this entry needs to wait for one of: // - user to be opted-in // - conversation to be active @@ -967,7 +971,7 @@ void ConversationDriver::SubmitHumanConversationEntry( return; } - DCHECK(turn->character_type == CharacterType::HUMAN); + DCHECK(latest_turn->character_type == CharacterType::HUMAN); is_request_in_progress_ = true; for (auto& obs : observers_) { @@ -975,16 +979,20 @@ void ConversationDriver::SubmitHumanConversationEntry( } // If it's a suggested question, remove it - auto found_question_iter = base::ranges::find(suggestions_, turn->text); + auto found_question_iter = + base::ranges::find(suggestions_, latest_turn->text); if (found_question_iter != suggestions_.end()) { suggestions_.erase(found_question_iter); OnSuggestedQuestionsChanged(); } // Directly modify Entry's text to remove engine-breaking substrings - engine_->SanitizeInput(turn->text); - if (turn->selected_text) { - engine_->SanitizeInput(*turn->selected_text); + if (!has_edits) { // Edits are already sanitized. + engine_->SanitizeInput(latest_turn->text); + } + + if (latest_turn->selected_text) { + engine_->SanitizeInput(*latest_turn->selected_text); } // TODO(petemill): Tokenize the summary question so that we @@ -992,19 +1000,20 @@ void ConversationDriver::SubmitHumanConversationEntry( // TODO(jocelyn): Assigning turn.type below is a workaround for now since // callers of SubmitHumanConversationEntry mojo API currently don't have // action_type specified. - std::string question_part = turn->text; - if (turn->action_type == mojom::ActionType::UNSPECIFIED) { - if (turn->text == l10n_util::GetStringUTF8(IDS_CHAT_UI_SUMMARIZE_PAGE)) { - turn->action_type = mojom::ActionType::SUMMARIZE_PAGE; + std::string question_part = latest_turn->text; + if (latest_turn->action_type == mojom::ActionType::UNSPECIFIED) { + if (latest_turn->text == + l10n_util::GetStringUTF8(IDS_CHAT_UI_SUMMARIZE_PAGE)) { + latest_turn->action_type = mojom::ActionType::SUMMARIZE_PAGE; question_part = l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_PAGE); - } else if (turn->text == + } else if (latest_turn->text == l10n_util::GetStringUTF8(IDS_CHAT_UI_SUMMARIZE_VIDEO)) { - turn->action_type = mojom::ActionType::SUMMARIZE_VIDEO; + latest_turn->action_type = mojom::ActionType::SUMMARIZE_VIDEO; question_part = l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_VIDEO); } else { - turn->action_type = mojom::ActionType::QUERY; + latest_turn->action_type = mojom::ActionType::QUERY; } } @@ -1169,8 +1178,7 @@ void ConversationDriver::SubmitSummarizationRequest() { CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_PAGE, ConversationTurnVisibility::VISIBLE, l10n_util::GetStringUTF8(IDS_CHAT_UI_SUMMARIZE_PAGE), std::nullopt, - std::nullopt, base::Time::Now(), - std::vector{}); + std::nullopt, base::Time::Now(), std::nullopt); SubmitHumanConversationEntry(std::move(turn)); } @@ -1348,17 +1356,32 @@ void ConversationDriver::ModifyConversation(uint32_t turn_index, std::string sanitized_input = new_text; engine_->SanitizeInput(sanitized_input); - if (sanitized_input.empty() || sanitized_input == turn->text) { + const auto& current_text = turn->edits && !turn->edits->empty() + ? turn->edits->back()->text + : turn->text; + if (sanitized_input.empty() || sanitized_input == current_text) { return; } - turn->edits.push_back(turn.Clone()); - turn->text = sanitized_input; - turn->last_edited_time = base::Time::Now(); + // turn->selected_text and turn->events are actually std::nullopt for + // editable human turns in our current implementation, just use std::nullopt + // here directly to be more explicit and avoid confusion. + auto edited_turn = mojom::ConversationTurn::New( + turn->character_type, turn->action_type, turn->visibility, + sanitized_input, std::nullopt /* selected_text */, + std::nullopt /* events */, base::Time::Now(), std::nullopt /* edits */); + if (!turn->edits) { + turn->edits.emplace(); + } + turn->edits->emplace_back(std::move(edited_turn)); // Modifying human turn, drop anything after this turn_index and resubmit. auto new_turn = std::move(chat_history_.at(turn_index)); chat_history_.erase(chat_history_.begin() + turn_index, chat_history_.end()); + for (auto& obs : observers_) { + obs.OnHistoryUpdate(); + } + SubmitHumanConversationEntry(std::move(new_turn)); } diff --git a/components/ai_chat/core/browser/conversation_driver_unittest.cc b/components/ai_chat/core/browser/conversation_driver_unittest.cc index c9d321936f32..3ca675af5bef 100644 --- a/components/ai_chat/core/browser/conversation_driver_unittest.cc +++ b/components/ai_chat/core/browser/conversation_driver_unittest.cc @@ -330,13 +330,11 @@ TEST_F(ConversationDriverUnitTest, SubmitSelectedText) { mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_SELECTED_TEXT), - "I have spoken.", std::nullopt, base::Time::Now(), - std::vector{})); + "I have spoken.", std::nullopt, base::Time::Now(), std::nullopt)); expected_history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "This is the way.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); EXPECT_EQ(history.size(), expected_history.size()); for (size_t i = 0; i < history.size(); i++) { EXPECT_TRUE(CompareConversationTurn(history[i], expected_history[i])); @@ -375,24 +373,20 @@ TEST_F(ConversationDriverUnitTest, SubmitSelectedText) { mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_SELECTED_TEXT), - "I have spoken.", std::nullopt, base::Time::Now(), - std::vector{})); + "I have spoken.", std::nullopt, base::Time::Now(), std::nullopt)); expected_history2.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "This is the way.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); expected_history2.push_back(mojom::ConversationTurn::New( mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, l10n_util::GetStringUTF8(IDS_AI_CHAT_QUESTION_SUMMARIZE_SELECTED_TEXT), - "I have spoken again.", std::nullopt, base::Time::Now(), - std::vector{})); + "I have spoken again.", std::nullopt, base::Time::Now(), std::nullopt)); expected_history2.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "This is the way.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); EXPECT_EQ(history2.size(), expected_history2.size()); for (size_t i = 0; i < history2.size(); i++) { EXPECT_TRUE(CompareConversationTurn(history2[i], expected_history2[i])); @@ -729,18 +723,16 @@ TEST_F(ConversationDriverUnitTest, ModifyConversation) { })); // Setup history for testing. - auto last_edited_time1 = base::Time::Now(); + auto created_time1 = base::Time::Now(); std::vector history; history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::HUMAN, mojom::ActionType::QUERY, mojom::ConversationTurnVisibility::VISIBLE, "prompt1", std::nullopt, - std::nullopt, last_edited_time1, - std::vector{})); + std::nullopt, created_time1, std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "answer1", std::nullopt, - std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, base::Time::Now(), std::nullopt)); conversation_driver_->SetChatHistoryForTesting(std::move(history)); // Modify an entry for the first time. @@ -748,30 +740,38 @@ TEST_F(ConversationDriverUnitTest, ModifyConversation) { const auto& conversation_history = conversation_driver_->GetConversationHistory(); ASSERT_EQ(conversation_history.size(), 1u); - EXPECT_EQ(conversation_history[0]->text, "prompt2"); - EXPECT_NE(conversation_history[0]->last_edited_time, last_edited_time1); - ASSERT_EQ(conversation_history[0]->edits.size(), 1u); - EXPECT_EQ(conversation_history[0]->edits.at(0)->text, "prompt1"); - EXPECT_EQ(conversation_history[0]->edits.at(0)->last_edited_time, - last_edited_time1); + EXPECT_EQ(conversation_history[0]->text, "prompt1"); + EXPECT_EQ(conversation_history[0]->created_time, created_time1); + + ASSERT_TRUE(conversation_history[0]->edits); + ASSERT_EQ(conversation_history[0]->edits->size(), 1u); + EXPECT_EQ(conversation_history[0]->edits->at(0)->text, "prompt2"); + EXPECT_NE(conversation_history[0]->edits->at(0)->created_time, created_time1); + EXPECT_FALSE(conversation_history[0]->edits->at(0)->edits); + WaitForOnEngineCompletionComplete(); ASSERT_EQ(conversation_history.size(), 2u); EXPECT_EQ(conversation_history[1]->text, "new answer"); - auto last_edited_time2 = conversation_history[0]->last_edited_time; + auto created_time2 = conversation_history[0]->edits->at(0)->created_time; // Modify the same entry again. conversation_driver_->ModifyConversation(0, "prompt3"); ASSERT_EQ(conversation_history.size(), 1u); - EXPECT_EQ(conversation_history[0]->text, "prompt3"); - EXPECT_NE(conversation_history[0]->last_edited_time, last_edited_time2); - ASSERT_EQ(conversation_history[0]->edits.size(), 2u); - EXPECT_EQ(conversation_history[0]->edits.at(0)->text, "prompt1"); - EXPECT_EQ(conversation_history[0]->edits.at(0)->last_edited_time, - last_edited_time1); - EXPECT_EQ(conversation_history[0]->edits.at(1)->text, "prompt2"); - EXPECT_EQ(conversation_history[0]->edits.at(1)->last_edited_time, - last_edited_time2); + EXPECT_EQ(conversation_history[0]->text, "prompt1"); + EXPECT_EQ(conversation_history[0]->created_time, created_time1); + + ASSERT_TRUE(conversation_history[0]->edits); + ASSERT_EQ(conversation_history[0]->edits->size(), 2u); + EXPECT_EQ(conversation_history[0]->edits->at(0)->text, "prompt2"); + EXPECT_EQ(conversation_history[0]->edits->at(0)->created_time, created_time2); + EXPECT_FALSE(conversation_history[0]->edits->at(0)->edits); + + EXPECT_EQ(conversation_history[0]->edits->at(1)->text, "prompt3"); + EXPECT_NE(conversation_history[0]->edits->at(1)->created_time, created_time1); + EXPECT_NE(conversation_history[0]->edits->at(1)->created_time, created_time2); + EXPECT_FALSE(conversation_history[0]->edits->at(1)->edits); + WaitForOnEngineCompletionComplete(); ASSERT_EQ(conversation_history.size(), 2u); EXPECT_EQ(conversation_history[1]->text, "new answer"); diff --git a/components/ai_chat/core/browser/engine/engine_consumer_claude_unittest.cc b/components/ai_chat/core/browser/engine/engine_consumer_claude_unittest.cc index a743648eb9c5..d50b2affe54d 100644 --- a/components/ai_chat/core/browser/engine/engine_consumer_claude_unittest.cc +++ b/components/ai_chat/core/browser/engine/engine_consumer_claude_unittest.cc @@ -73,12 +73,11 @@ TEST_F(EngineConsumerClaudeUnitTest, TestGenerateAssistantResponse) { mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, "Which show is this catchphrase from?", "I have spoken.", std::nullopt, - base::Time::Now(), std::vector{})); + base::Time::Now(), std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "The Mandalorian.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); auto* mock_remote_completion_client = GetMockRemoteCompletionClient(); std::string prompt_before_time_and_date = "\n\nHuman: Here is the text of a web page in tags:\n\nThis " diff --git a/components/ai_chat/core/browser/engine/engine_consumer_conversation_api_unittest.cc b/components/ai_chat/core/browser/engine/engine_consumer_conversation_api_unittest.cc index 1092b3a81ab0..bdfe629412b0 100644 --- a/components/ai_chat/core/browser/engine/engine_consumer_conversation_api_unittest.cc +++ b/components/ai_chat/core/browser/engine/engine_consumer_conversation_api_unittest.cc @@ -215,17 +215,16 @@ TEST_F(EngineConsumerConversationAPIUnitTest, mojom::CharacterType::HUMAN, mojom::ActionType::QUERY, mojom::ConversationTurnVisibility::VISIBLE, "Which show is this catchphrase from?", "I have spoken.", std::nullopt, - base::Time::Now(), std::vector{})); + base::Time::Now(), std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "The Mandalorian.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::HUMAN, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "Is it related to a broader series?", std::nullopt, std::nullopt, - base::Time::Now(), std::vector{})); + base::Time::Now(), std::nullopt)); std::string expected_events = R"([ {"role": "user", "type": "pageText", "content": "This is my page. I have spoken."}, {"role": "user", "type": "pageExcerpt", "content": "I have spoken."}, diff --git a/components/ai_chat/core/browser/engine/engine_consumer_llama_unittest.cc b/components/ai_chat/core/browser/engine/engine_consumer_llama_unittest.cc index b1f758f13c93..e01839d71703 100644 --- a/components/ai_chat/core/browser/engine/engine_consumer_llama_unittest.cc +++ b/components/ai_chat/core/browser/engine/engine_consumer_llama_unittest.cc @@ -71,12 +71,11 @@ TEST_F(EngineConsumerLlamaUnitTest, TestGenerateAssistantResponse) { mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, "Which show is this catchphrase from?", "This is the way.", std::nullopt, - base::Time::Now(), std::vector{})); + base::Time::Now(), std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, "The Mandalorian.", - std::nullopt, std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, std::nullopt, base::Time::Now(), std::nullopt)); auto* mock_remote_completion_client = static_cast(engine_->GetAPIForTesting()); std::string prompt_before_time_and_date = diff --git a/components/ai_chat/core/browser/engine/engine_consumer_oai_unittest.cc b/components/ai_chat/core/browser/engine/engine_consumer_oai_unittest.cc index 05a54a9d2127..26129c828b36 100644 --- a/components/ai_chat/core/browser/engine/engine_consumer_oai_unittest.cc +++ b/components/ai_chat/core/browser/engine/engine_consumer_oai_unittest.cc @@ -207,14 +207,12 @@ TEST_F(EngineConsumerOAIUnitTest, TestGenerateAssistantResponse) { history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::HUMAN, mojom::ActionType::SUMMARIZE_SELECTED_TEXT, mojom::ConversationTurnVisibility::VISIBLE, human_input, selected_text, - std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, base::Time::Now(), std::nullopt)); history.push_back(mojom::ConversationTurn::New( mojom::CharacterType::ASSISTANT, mojom::ActionType::RESPONSE, mojom::ConversationTurnVisibility::VISIBLE, assistant_input, std::nullopt, - std::nullopt, base::Time::Now(), - std::vector{})); + std::nullopt, base::Time::Now(), std::nullopt)); std::string expected_human_input = base::StrCat({base::ReplaceStringPlaceholders( diff --git a/components/ai_chat/core/common/mojom/ai_chat.mojom b/components/ai_chat/core/common/mojom/ai_chat.mojom index cf32eec45f74..5c09e928020d 100644 --- a/components/ai_chat/core/common/mojom/ai_chat.mojom +++ b/components/ai_chat/core/common/mojom/ai_chat.mojom @@ -148,8 +148,14 @@ struct ConversationTurn { // be important. array? events; - mojo_base.mojom.Time last_edited_time; - array edits; + mojo_base.mojom.Time created_time; + + // Edits to this turn, sorted by time of creation, with the most recent edit + // at the end of the array. When this appears, the value of |text| field is + // the original text of the turn, the last entry of this array should be used + // instead of the original turn text when submitting the turn to the AI + // engine or displaying the most recent text to users. + array? edits; }; // Represents an AI engine model choice, usually for the user to choose for a diff --git a/components/ai_chat/resources/page/components/conversation_list/index.tsx b/components/ai_chat/resources/page/components/conversation_list/index.tsx index e4cd0c189b8a..9ecdce7138c0 100644 --- a/components/ai_chat/resources/page/components/conversation_list/index.tsx +++ b/components/ai_chat/resources/page/components/conversation_list/index.tsx @@ -113,9 +113,10 @@ function ConversationList(props: ConversationListProps) { const showLongPageContentInfo = id === 1 && isAIAssistant && context.shouldShowLongPageWarning const showEditInput = editInputId === id - const showEditIndicator = - !isAIAssistant && !turn.selectedText && !showEditInput && - turn.edits.length > 0 + const showEditIndicator = !showEditInput && !!turn.edits?.length + const latestEdit = turn.edits?.at(-1); + const latestTurnText = latestEdit?.text ?? turn.text + const lastEditedTime = latestEdit?.createdTime ?? turn.createdTime const turnContainer = classnames({ [styles.turnContainerMobile]: context.isMobile, @@ -139,7 +140,7 @@ function ConversationList(props: ConversationListProps) { if (!event?.completionEvent) return navigator.clipboard.writeText(event.completionEvent.completion) } else { - navigator.clipboard.writeText(turn.text) + navigator.clipboard.writeText(latestTurnText) } } @@ -192,14 +193,14 @@ function ConversationList(props: ConversationListProps) { isEntryInProgress={isEntryInProgress} /> )} - {!isAIAssistant && !turn.selectedText && - !showEditInput && turn.text} + {!isAIAssistant && !turn.selectedText && !showEditInput && + latestTurnText} {showEditIndicator && ( - + )} - {!isAIAssistant && !turn.selectedText && showEditInput && ( + {showEditInput && ( handleEditSubmit(id, text)} onCancel={() => setEditInputId(null)} /> diff --git a/components/ai_chat/resources/page/stories/components_panel.tsx b/components/ai_chat/resources/page/stories/components_panel.tsx index 31c05b78040f..fe43e3d77f70 100644 --- a/components/ai_chat/resources/page/stories/components_panel.tsx +++ b/components/ai_chat/resources/page/stories/components_panel.tsx @@ -54,7 +54,7 @@ const HISTORY: mojom.ConversationTurn[] = [ actionType: mojom.ActionType.SUMMARIZE_PAGE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -64,7 +64,7 @@ const HISTORY: mojom.ConversationTurn[] = [ actionType: mojom.ActionType.UNSPECIFIED, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getCompletionEvent('The ways that animals move are just about as myriad as the animal kingdom itself. They walk, run, swim, crawl, fly and slither — and within each of those categories lies a tremendous number of subtly different movement types. A seagull and a *hummingbird* both have wings, but otherwise their flight techniques and abilities are poles apart. Orcas and **piranhas** both have tails, but they accomplish very different types of swimming. Even a human walking or running is moving their body in fundamentally different ways.')] }, { @@ -74,7 +74,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -84,7 +84,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getCompletionEvent(`## How We Created an Accessible, Scalable Color Palette\n\nDuring the latter part of 2021, I reflected on the challenges we were facing at Modern Health. One recurring problem that stood out was our struggle to create new products with an unstructured color palette. This resulted in poor [communication](https://www.google.com) between designers and developers, an inconsistent product brand, and increasing accessibility problems.\n\n1. Inclusivity: our palette provides easy ways to ensure our product uses accessible contrasts.\n 2. Efficiency: our palette is diverse enough for our current and future product design, yet values are still predictable and constrained.\n 3. Reusability: our palette is on-brand but versatile. There are very few one-offs that fall outside the palette.\n\n This article shares the process I followed to apply these principles to develop a more adaptable color palette that prioritizes accessibility and is built to scale into all of our future product **design** needs.`)] }, { @@ -94,7 +94,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -104,7 +104,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getCompletionEvent('The partial sum formed by the first n + 1 terms of a Taylor series is a polynomial of degree n that is called the nth Taylor polynomial of the function. Taylor polynomials are approximations of a function, which become generally better as n increases.')] }, { @@ -114,7 +114,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -124,7 +124,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getCompletionEvent("Hello! As a helpful and respectful AI assistant, I'd be happy to assist you with your question. However, I'm a text-based AI and cannot provide code in a specific programming language like C++. Instead, I can offer a brief explanation of how to write a \"hello world\" program in C++.\n\nTo write a \"hello world\" program in C++, you can use the following code:\n\n```c++\n#include \n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}\n```\nThis code will print \"Hello, world!\" and uses `iostream` std library. If you have any further questions or need more information, please don't hesitate to ask!")] }, { @@ -134,7 +134,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: 'Pointer compression is a memory optimization technique where pointers (memory addresses) are stored in a compressed format to save memory. The basic idea is that since most pointers will be clustered together and point to objects allocated around the same time, you can store a compressed representation of the pointer and decompress it when needed. Some common ways this is done: Store an offset from a base pointer instead of the full pointer value Store increments/decrements from the previous pointer instead of the full value Use pointer tagging to store extra information in the low bits of the pointer Encode groups of pointers together The tradeoff is some extra CPU cost to decompress the pointers, versus saving memory. This technique is most useful in memory constrained environments.', edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -144,7 +144,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getCompletionEvent('Pointer compression is a memory optimization technique where pointers are stored in a compressed format to save memory.')] }, { @@ -154,7 +154,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: 'Pointer compression is a memory optimization technique where pointers are stored in a compressed format to save memory.', edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -164,7 +164,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getSearchStatusEvent(), getSearchEvent(['pointer compression', 'c++ language specification']), getCompletionEvent('Pointer compression is a memory optimization technique.')] }, { @@ -179,11 +179,11 @@ const HISTORY: mojom.ConversationTurn[] = [ actionType: mojom.ActionType.SHORTEN, visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: '', - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, edits: [], events: [] }], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [] }, { @@ -193,7 +193,7 @@ const HISTORY: mojom.ConversationTurn[] = [ visibility: mojom.ConversationTurnVisibility.VISIBLE, selectedText: undefined, edits: [], - lastEditedTime: { internalValue: BigInt('13278618001000000') }, + createdTime: { internalValue: BigInt('13278618001000000') }, events: [getSearchStatusEvent(), getSearchEvent(['LTT store backpack dimensions', 'Tesla Model Y frunk dimensions'])] } ] diff --git a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift index 2717246aba75..d3b3cdef5563 100644 --- a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift @@ -645,8 +645,8 @@ struct AIChatView_Preview: PreviewProvider { "After months of leaks and some recent coordinated teases from the company itself, Sonos is finally officially announcing the Era 300 and Era 100 speakers. Both devices go up for preorder today — the Era 300 costs $449 and the Era 100 is $249 — and they’ll be available to purchase in stores beginning March 28th.\n\nAs its unique design makes clear, the Era 300 represents a completely new type of speaker for the company; it’s designed from the ground up to make the most of spatial audio music and challenge competitors like the HomePod and Echo Studio.", selectedText: nil, events: nil, - lastEditedTime: Date.now, - edits: [] + createdTime: Date.now, + edits: nil ), isEntryInProgress: false ) diff --git a/ios/brave-ios/Sources/AIChat/Components/Messages/AIChatResponseMessageView.swift b/ios/brave-ios/Sources/AIChat/Components/Messages/AIChatResponseMessageView.swift index 0d2ef0bd8c20..690d399ae1c3 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Messages/AIChatResponseMessageView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Messages/AIChatResponseMessageView.swift @@ -232,8 +232,8 @@ struct AIChatResponseMessageView_Previews: PreviewProvider { "After months of leaks and some recent coordinated teases from the company itself, Sonos is finally officially announcing the Era 300 and Era 100 speakers. Both devices go up for preorder today — the Era 300 costs $449 and the Era 100 is $249 — and they’ll be available to purchase in stores beginning March 28th.\n\nAs its unique design makes clear, the Era 300 represents a completely new type of speaker for the company; it’s designed from the ground up to make the most of spatial audio music and challenge competitors like the HomePod and Echo Studio.", selectedText: nil, events: nil, - lastEditedTime: Date.now, - edits: [] + createdTime: Date.now, + edits: nil ), isEntryInProgress: false ) diff --git a/ios/browser/api/ai_chat/ai_chat.mm b/ios/browser/api/ai_chat/ai_chat.mm index 4ceb8cbaa648..62fda48cf1ca 100644 --- a/ios/browser/api/ai_chat/ai_chat.mm +++ b/ios/browser/api/ai_chat/ai_chat.mm @@ -108,7 +108,7 @@ - (void)submitHumanConversationEntry:(NSString*)text { ai_chat::mojom::ActionType::UNSPECIFIED, ai_chat::mojom::ConversationTurnVisibility::VISIBLE, base::SysNSStringToUTF8(text), std::nullopt, std::nullopt, - base::Time::Now(), std::vector{})); + base::Time::Now(), std::nullopt)); } - (void)submitSummarizationRequest {