Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Leo): Support modifying user prompts #24558

Merged
merged 1 commit into from
Jul 17, 2024
Merged

Conversation

yrliou
Copy link
Member

@yrliou yrliou commented Jul 8, 2024

Resolves brave/brave-browser#35342

Design link: https://www.figma.com/design/m0Gdbf0wtqyfEFGm32VLLc/%F0%9F%94%84-Leo-%5BIN-PROGRESS%5D?node-id=4604-59626&t=JIZyytWeZVk4jjMw-4

Note that the edit history part is not implemented yet by this PR.

Editing:
Screenshot 2024-07-10 at 5 27 33 PM
Screenshot 2024-07-10 at 5 27 42 PM

Edited:
Screenshot 2024-07-08 at 3 33 26 PM

Submitter Checklist:

  • I confirm that no security/privacy review is needed and no other type of reviews are needed, or that I have requested them
  • There is a ticket for my issue
  • Used Github auto-closing keywords in the PR description above
  • Wrote a good PR/commit description
  • Squashed any review feedback or "fixup" commits before merge, so that history is a record of what happened in the repo, not your PR
  • Added appropriate labels (QA/Yes or QA/No; release-notes/include or release-notes/exclude; OS/...) to the associated issue
  • Checked the PR locally:
    • npm run test -- brave_browser_tests, npm run test -- brave_unit_tests wiki
    • npm run presubmit wiki, npm run gn_check, npm run tslint
  • Ran git rebase master (if needed)

Reviewer Checklist:

  • A security review is not needed, or a link to one is included in the PR description
  • New files have MPL-2.0 license header
  • Adequate test coverage exists to prevent regressions
  • Major classes, functions and non-trivial code blocks are well-commented
  • Changes in component dependencies are properly reflected in gn
  • Code follows the style guide
  • Test plan is specified in PR before merging

After-merge Checklist:

Test Plan:

Edit a user prompt in Leo side panel, it should remove entries after it and get a new answer.

@yrliou yrliou requested review from petemill and nullhook July 8, 2024 22:37
@yrliou yrliou self-assigned this Jul 8, 2024
@github-actions github-actions bot added CI/storybook-url Deploy storybook and provide a unique URL for each build puLL-Merge labels Jul 8, 2024
@yrliou yrliou requested a review from a team as a code owner July 8, 2024 23:07
@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from b7adf3f to 6b164b8 Compare July 8, 2024 23:16
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

</div>
)}
<div className={styles.turnActions}>
<CopyButton onClick={handleCopyText} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can copy human text now? i thought copy was only allowed on assitant turns.

Copy link
Member Author

@yrliou yrliou Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-07-08 at 9 32 53 PM

I think so as it's how it looks in the figma.

align-self: stretch;
}

icon {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean leo-icon? also, you can directly modify the size in .editIndicator style with css ex. --leo-icon-size: 16px

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip, fixed in 1613bf9.

onCancel: () => void
}

function EditInput(props: Props) {
Copy link
Contributor

@nullhook nullhook Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the positions and background of the action buttons be tweaked when the input box has multiple lines?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Member Author

@yrliou yrliou Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2024-07-10 at 5 27 42 PM

Updated after updated design.

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from 8244582 to 62a9a67 Compare July 9, 2024 17:01
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from 62a9a67 to 2c6bbca Compare July 9, 2024 23:50
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@@ -147,6 +152,9 @@ struct ConversationTurn {
// to be an event - as the events become richer, the order around text could
// be important.
array<ConversationEntryEvent>? events;

mojo_base.mojom.Time last_edited_time;
array<EditEntry> edits;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels to me like this could be a ConversationEntryEvent instead of introducing a new property. Also potentially makes data storage simpler.

We could have a UserSubmissionEvent with a text, timestamp property.

Alternatively perhaps it's more appropriate to have an entirely new ConversationTurn for edits? That would allow:

  • edited turns to have any properties / events that regular turns have
  • simple data storage (in the upcoming SQL)
  • potential for forking the conversation and maintaining history
    This could happen via a property on ConversationTurn:
array<ConversationTurn> edits;

What do you think @yrliou?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do the latter, we can definitely keep this PR to only allow a single conversation thread, but it would keep open the option to store and display multiple conversation forks, like competing products do.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly that it has to be inside the events, having it outside seems intuitive to me rather than an entry in an array of unions. I wonder why would this make data storage harder, it doesn't seem very different than putting it in the events?
For later, currently it seems unnecessary to me to have it as a ConversationTurn, like, do we really need to edit other things in the turn?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel strongly that it has to be inside the events, having it outside seems intuitive to me rather than an entry in an array of unions. I wonder why would this make data storage harder, it doesn't seem very different than putting it in the events?

Because we are changing the structure from a simple string text property to an array of events. This has already been done for the assistant responses. And we already have storage setup for the events. Soon we'll either do the same for the user messages, or move away from ConversationTurn being the same struct for both assistant responses and user messages. If we're not going to do the below, I'm indifferent for the moment whether we keep this extra field of migrate to events. It doesn't have to be in this PR, we can do it in the storage PR.

For later, currently it seems unnecessary to me to have it as a ConversationTurn, like, do we really need to edit other things in the turn?

Yes ConversationTurn is already a simple struct that has all the fields we might want to edit - including action_type and text. I can definitely see the user wanting to change action types when not getting the desired response. Seems better than duplicating fields in a separate struct for an Edit. And with ConversationTurn objects for edits, we can very simply construct the thread for each edit with a Parent / Child relationship if in the database each ConversationTurn maintains a ParentConversationTurnId field. This schema would prevent us having to modify the database schema when we want to preserve the pre-edit history and not delete it.

Can we try it here? I don't see that it will change anything too drastic?

If we want to support threading but keep the new field then we'll probably add a new ConversationTurnEdits table in the database, and maintain a relationship to ConversationTurn (aka ConversationEntry).

Copy link
Member Author

@yrliou yrliou Jul 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure if we want to clone a complete turn struct and preferred that we only copy needed things, but I'm okay with it, I'm changing to the array edits now.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@petemill fixed in a43b318

@yrliou yrliou requested a review from a team as a code owner July 11, 2024 00:02
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@petemill
Copy link
Member

I noticed that the edit button is not working when there is an associated action with the message. I have asked at brave/brave-browser#35342 (comment) whether this should be editable. If it shouldn't then how about hiding the edit button from these messages?

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from c010812 to 9c465a2 Compare July 11, 2024 19:38
@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@@ -264,6 +267,7 @@ interface PageHandler {
ClosePanel();
GetActionMenuList() => (array<ActionGroup> action_list);
OpenModelSupportUrl();
ModifyConversation(uint32 turn_index, string new_text);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ModifyConversationEntry seems more intuitive?

}

std::string sanitized_input = new_text;
engine_->SanitizeInput(sanitized_input);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SanitizeInput is already called by SubmitHumanConversationEntry. Do you think we need to call it twice for the purpose of being able to ignore it if it matches the existing turn text?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally called here so we can check here, I've added a check to avoid another sanitization happening in 26769e4.


// 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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're modifying chat_history_ then technically shouldn't we let the observers know? Or would you rather wait for the edited turn to appear in the history after calling SubmitHumanConversationEntry?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I didn't do so since it would be called in SubmitHumanConversationEntry, but I'm fine having another one here after we clear it. Added in 26769e4.

@@ -147,6 +147,9 @@ struct ConversationTurn {
// to be an event - as the events become richer, the order around text could
// be important.
array<ConversationEntryEvent>? events;

mojo_base.mojom.Time last_edited_time;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might end up naming this time_created or something like that, so that we can order ConversationTurn and edits in the database, and get the last edited time by looking at the items in edits. Not saying you should change this in this PR, just saying in case you have a different opinion.

Example to illustrate the ergonomics:

  {
    text: 'Will an LTT store backpack fit in a Tesla Model Y frunk?',
    characterType: mojom.CharacterType.HUMAN,
    actionType: mojom.ActionType.SHORTEN,
    visibility: mojom.ConversationTurnVisibility.VISIBLE,
    selectedText: '',
    edits: [{
      text: 'Will a lux LTT store backpack fit in a 2024 Tesla Model Y frunk?',
      characterType: mojom.CharacterType.HUMAN,
      actionType: mojom.ActionType.SHORTEN,
      visibility: mojom.ConversationTurnVisibility.VISIBLE,
      selectedText: '',
      time_created: { internalValue: BigInt('13278618001000000') },
      edits: [],
      events: []
    }],
    time_created: { internalValue: BigInt('13278618000999999') },
    events: []
  },
  {

Then we don't have to worry about modifying times, etc - we just look at each original or edit and know the time it was created.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rename to created_time in 26769e4

Comment on lines 1355 to 1357
turn->edits.push_back(turn.Clone());
turn->text = sanitized_input;
turn->last_edited_time = base::Time::Now();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you edit an already edited ConversationTurn, won't this mean that we'll end up with a tree of edits instead of a flat list? It seems to me that it's simpler to put all the edits on the original turn.

Cloning a turn that already has edits and pushing that to the edits array seems like we'll end up with:

  • Turn1
  • Turn2
    • EditedTurn1
    • EditedTurn2
      • EditedTurn1
      • EditedTurn2

I'm fine if you want to reduce the modifications by modifying turn.text as you are doing here, and have the original as an "edit" (see below - [1]). But I do think we should at least just have a single array of edits on a ConversationTurn and not populate any edits on the edited entries, i.e. (if you continue putting the old version in edits and make the new version the root turn):

  auto previous_version = turn.Clone();
  previous_version.edits.clear(); // or ideally previous_version.edits = std::nullopt
  turn->edits.push_back(std::move(previous_version));
  turn->text = sanitized_input;
  turn->last_edited_time = base::Time::Now();

Ending up with:

  • Turn1
  • Turn2
    • EditedTurn1
    • EditedTurn2

[1] That seems counterintuitive compared to the name of the property: "edit". I would originally have thought that we'd keep the original text at turn.text and simply have the edited versions on turn.edits. Not doing that does mean that you don't need to change SubmitHumanConversationEntry, or the rendering in WebUI and iOS. However, those changes would be simple since the edit is still a ConversationTurn (either the render function uses the given ConversationTurn or it uses the latest ConversationTurn from turn.edits. Again, I'm fine if you don't want to make that change in this PR for whatever reason since it supports the feature in this basic form pre-storage and before we support keeping the history of edits.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've updated to put edits in the edits array without cloning edits array in the original turn, and keep original turn in the root in 26769e4.

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should assert the edits don't have any edits

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import * as React from 'react'
import Icon from '@brave/leo/react/icon'
import styles from './style.module.scss'
import Button from '@brave/leo/react/button'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put external import above relative file path import

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return (
<div className={styles.editIndicator}>
<Icon name='edit-pencil' />
<span className={styles.editedText}>Edited</span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This word needs to be localized

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Button onClick={props.onClick} className={styles.editButton}
fab
size='tiny'
kind='plain-faint'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a title attribute here, for a tooltip and accessibility

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

className={styles.growWrap}
data-replicated-value={text}
>
<textarea
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't have a label for this, should we at least have an aria-placeholder attribute. Something like: "Edited message"? Or because we have the existing message content in the field, we don't need it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't put a placeholder as we would have the existing message content here.

@yrliou yrliou requested a review from a team as a code owner July 16, 2024 22:06
@@ -71,7 +71,7 @@ function DataContextProvider(props: DataContextProviderProps) {
const [allModels, setAllModels] = React.useState<mojom.Model[]>([])
const [conversationHistory, setConversationHistory] = React.useState<mojom.ConversationTurn[]>([])
const [suggestedQuestions, setSuggestedQuestions] = React.useState<string[]>([])
const [isGenerating, setIsGenerating] = React.useState(false)
const [isGenerating, setIsGenerating] = React.useState(props.store?.isGenerating || false)
Copy link
Member Author

@yrliou yrliou Jul 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like an existing bug here to me when I was working on this commit.

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from 39030ce to 0670c33 Compare July 16, 2024 22:20
@@ -203,6 +203,7 @@ function ConversationList(props: ConversationListProps) {
text={latestTurnText}
onSubmit={(text) => handleEditSubmit(id, text)}
onCancel={() => setEditInputId(null)}
isSubmitDisabled={shouldDisableUserInput}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we don't expect to handle another request when answer is still generating, so disable the submission here.
For reference, ChatGPT UI also allows to click on edit button and disables the submit edit button when the answer is generating.

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

Copy link
Collaborator

@mkarolin mkarolin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings++
chromium_src++

@yrliou yrliou requested review from petemill and nullhook July 17, 2024 17:32

.time {
color: var(--leo-color-text-tertiary);
text-decoration: underline;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we should wait to make it appear interactive until we implement this part of the feature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, removed.

function EditInput(props: Props) {
const [text, setText] = React.useState(props.text)
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this handler needs to check props.isSubmitDisabled

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in the final squashed commit.

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from 0670c33 to e012971 Compare July 17, 2024 18:16
Copy link
Contributor

[puLL-Merge] - brave/brave-core@24558

Description

This PR introduces the ability to edit human conversation turns in the AI chat feature. It adds new UI components for editing, displaying edit indicators, and handling the editing process. The backend has been updated to support modifying conversations and storing edit history.

Changes

Changes

  1. browser/ai_chat/android/ai_chat_utils.cc:

    • Added base::Time::Now() to ConversationTurn creation.
  2. browser/ui/webui/ai_chat/ai_chat_ui_page_handler.cc and .h:

    • Added ModifyConversation method to handle conversation edits.
  3. chromium_src/chrome/browser/autocomplete/chrome_autocomplete_provider_client.cc:

    • Updated ConversationTurn creation to include timestamp.
  4. components/ai_chat/core/browser/constants.cc:

    • Added new localized strings for edit-related UI elements.
  5. components/ai_chat/core/browser/conversation_driver.cc and .h:

    • Implemented ModifyConversation method to handle editing of conversation turns.
    • Updated SubmitHumanConversationEntry to handle edits.
  6. components/ai_chat/core/common/mojom/ai_chat.mojom:

    • Added created_time and edits fields to ConversationTurn struct.
    • Added ModifyConversation method to PageHandler interface.
  7. components/ai_chat/resources/page/:

    • Added new components: EditButton, EditIndicator, and EditInput.
    • Updated ConversationList to support editing functionality.
    • Modified DataContextProvider to handle editing state.
  8. components/resources/ai_chat_ui_strings.grdp:

    • Added new strings for edit-related UI elements.
  9. iOS-specific changes:

    • Updated AIChatView.swift and AIChatResponseMessageView.swift to include createdTime and edits fields.
    • Modified ai_chat.mm to include timestamp in ConversationTurn creation.

Possible Issues

  1. The editing functionality is currently limited to human turns and does not support editing AI assistant responses.
  2. There might be potential race conditions if a user tries to edit a message while a new response is being generated.

Security Hotspots

  1. User input sanitization: Ensure that the SanitizeInput function is properly handling all possible input scenarios to prevent XSS or other injection attacks.
  2. Time-based information leakage: The addition of timestamps to conversation turns could potentially leak sensitive timing information. Ensure that this doesn't expose any unintended information about the system or user behavior.

@yrliou yrliou force-pushed the ai_chat_prompt_edit branch from e012971 to c24b05a Compare July 17, 2024 18:51
Copy link
Collaborator

@StephenHeaps StephenHeaps left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iOS++

@brave-builds
Copy link
Collaborator

A Storybook has been deployed to preview UI for the latest push

@bbondy bbondy merged commit 3f8e345 into master Jul 17, 2024
16 checks passed
@bbondy bbondy deleted the ai_chat_prompt_edit branch July 17, 2024 21:27
@github-actions github-actions bot added this to the 1.70.x - Nightly milestone Jul 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CI/storybook-url Deploy storybook and provide a unique URL for each build puLL-Merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add the ability to modify prompts and have it resubmit and truncate the history
7 participants