Skip to content

Commit

Permalink
improve handling of human message cell states, show visual indicator …
Browse files Browse the repository at this point in the history
…of message submission
  • Loading branch information
sqs committed May 27, 2024
1 parent 8b6c02c commit b2f8f05
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 33 deletions.
2 changes: 2 additions & 0 deletions vscode/webviews/chat/Transcript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const Transcript: React.FunctionComponent<{
chatEnabled={chatEnabled}
isFirstMessage={messageIndexInTranscript === 0}
isSent={true}
isPendingResponse={isLastHumanMessage && isLoading}
onSubmit={(
editorValue: SerializedPromptEditorValue,
addEnhancedContext: boolean
Expand Down Expand Up @@ -125,6 +126,7 @@ export const Transcript: React.FunctionComponent<{
message={null}
isFirstMessage={transcript.length === 0}
isSent={false}
isPendingResponse={false}
userInfo={userInfo}
chatEnabled={chatEnabled}
isEditorInitiallyFocused={true}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,41 @@ const meta: Meta<typeof HumanMessageCell> = {
component: HumanMessageCell,

args: {
message: null,
userInfo: FIXTURE_USER_ACCOUNT_INFO,
onSubmit: () => {},
__storybook__focus: false,
},

decorators: [VSCodeCell],
}

export default meta

export const FirstMessageEmpty: StoryObj<typeof meta> = {
export const NonEmptyFirstMessage: StoryObj<typeof meta> = {
args: {
isFirstMessage: true,
message: FIXTURE_TRANSCRIPT.explainCode2[0],
__storybook__focus: true,
},
}

export const FirstMessageWithText: StoryObj<typeof meta> = {
export const EmptyFollowup: StoryObj<typeof meta> = {
args: {
message: FIXTURE_TRANSCRIPT.explainCode2[0],
isFirstMessage: true,
message: null,
__storybook__focus: true,
},
}

export const FollowupEmpty: StoryObj<typeof meta> = {
export const SentPending: StoryObj<typeof meta> = {
args: {
message: null,
isFirstMessage: false,
message: FIXTURE_TRANSCRIPT.explainCode2[0],
isSent: true,
isPendingResponse: true,
__storybook__focus: true,
},
}

export const FollowupWithText: StoryObj<typeof meta> = {
export const SentComplete: StoryObj<typeof meta> = {
args: {
message: FIXTURE_TRANSCRIPT.explainCode2[0],
isFirstMessage: false,
isSent: true,
},
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export const HumanMessageCell: FunctionComponent<{
/** Whether this editor is for a message that has been sent already. */
isSent: boolean

/** Whether this editor is for a message whose assistant response is in progress. */
isPendingResponse: boolean

onChange?: (editorState: SerializedPromptEditorValue) => void
onSubmit: (editorValue: SerializedPromptEditorValue, addEnhancedContext: boolean) => void

Expand All @@ -43,6 +46,7 @@ export const HumanMessageCell: FunctionComponent<{
userContextFromSelection,
isFirstMessage,
isSent,
isPendingResponse,
onChange,
onSubmit,
isEditorInitiallyFocused,
Expand All @@ -68,6 +72,7 @@ export const HumanMessageCell: FunctionComponent<{
}
isFirstMessage={isFirstMessage}
isSent={isSent}
isPendingResponse={isPendingResponse}
onChange={onChange}
onSubmit={onSubmit}
disabled={!chatEnabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
}

/* Dim toolbar when unfocused. */
.container:not(:is(:not(.sent), .focused, :focus-within, :hover)) .toolbar {
.container:not(:is(:not(.sent-complete), .focused, :hover)) .toolbar {
opacity: 0.5;
}

/* Make the whole container look like a big input field when focused. */
.container:is(:not(.sent), .focused, :hover) {
.container:is(:not(.sent-complete), .focused, :hover) {
background-color: var(--vscode-input-background);
color: var(--vscode-input-foreground);

Expand All @@ -41,11 +41,11 @@
}

/* Hide the toolbar unless focused on all but the last cell. */
[role="row"]:has(.container:is(:not(.sent), .focused, :focus-within)) {
[role="row"]:has(.container:is(:not(.sent-complete), .focused)) {
/* HACK: Change HumanMessageCell style. */
padding-bottom: var(--human-message-editor-cell-spacing-bottom);
}
.container.sent:not(:is(.focused, :focus-within)) {
.container.sent-complete:not(.focused) {
margin-bottom: calc(var(--human-message-editor-toolbar-height) + var(--human-message-editor-gap) - (var(--cell-spacing) - var(--human-message-editor-cell-spacing-bottom)));
.toolbar {
display: none;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import type { ComponentProps } from 'react'
import { type Mock, describe, expect, test, vi } from 'vitest'
import { type Assertion, type Mock, describe, expect, test, vi } from 'vitest'
import { AppWrapper } from '../../../../../AppWrapper'
import { serializedPromptEditorStateFromText } from '../../../../../promptEditor/PromptEditor'
import { FILE_MENTION_EDITOR_STATE_FIXTURE } from '../../../../../promptEditor/fixtures'
import { FIXTURE_USER_ACCOUNT_INFO } from '../../../../fixtures'
import { HumanMessageEditor } from './HumanMessageEditor'
Expand Down Expand Up @@ -30,15 +31,87 @@ describe('HumanMessageEditor', () => {
expect(screen.getByRole('textbox')).toBeInTheDocument()
})

describe('states', () => {
function expectState(
{ container, mentionButton, submitButton }: ReturnType<typeof renderWithMocks>,
expected: {
toolbarVisible?: boolean
submitButtonEnabled?: boolean
submitButtonText?: string
}
): void {
if (expected.toolbarVisible !== undefined) {
notUnless(expect.soft(mentionButton), expected.toolbarVisible).toBeVisible()
notUnless(expect.soft(submitButton), expected.toolbarVisible).toBeVisible()
}

if (expected.submitButtonEnabled !== undefined) {
notUnless(expect.soft(submitButton), expected.submitButtonEnabled).toBeEnabled()
}
if (expected.submitButtonText !== undefined) {
expect.soft(submitButton).toHaveTextContent(expected.submitButtonText)
}

function notUnless<T>(assertion: Assertion<T>, value: boolean): Assertion<T> {
return value ? assertion : assertion.not
}
}

test('unsent', () => {
expectState(
renderWithMocks({
initialEditorState: serializedPromptEditorStateFromText('abc'),
isSent: false,
}),
{ toolbarVisible: true, submitButtonEnabled: true, submitButtonText: 'Send' }
)
})

describe('sent pending', () => {
function renderSentPending(): ReturnType<typeof renderWithMocks> {
const rendered = renderWithMocks({
initialEditorState: serializedPromptEditorStateFromText('abc'),
isSent: true,
isEditorInitiallyFocused: true,
isPendingResponse: true,
})
typeInEditor(rendered.editor, 'x')
fireEvent.keyDown(rendered.editor, ENTER_KEYBOARD_EVENT_DATA)
return rendered
}

test('initial', () => {
const rendered = renderSentPending()
expectState(rendered, {
toolbarVisible: true,
submitButtonEnabled: false,
submitButtonText: 'Send',
})
})
})

test('sent complete', () => {
const rendered = renderWithMocks({
initialEditorState: undefined,
isSent: true,
})
expectState(rendered, { toolbarVisible: false })

fireEvent.focus(rendered.editor)
expectState(rendered, { toolbarVisible: true })
})
})

describe('submitting', () => {
test('empty editor', () => {
const { container, submitButton, onSubmit } = renderWithMocks({
initialEditorState: undefined,
__test_dontTemporarilyDisableSubmit: true,
})
expect(submitButton).toBeDisabled()

// Click
fireEvent.click(submitButton)
fireEvent.click(submitButton!)
expect(onSubmit).toHaveBeenCalledTimes(0)

// Enter
Expand All @@ -50,11 +123,12 @@ describe('HumanMessageEditor', () => {
test('submit', async () => {
const { container, submitButton, onSubmit } = renderWithMocks({
initialEditorState: FILE_MENTION_EDITOR_STATE_FIXTURE,
__test_dontTemporarilyDisableSubmit: true,
})
expect(submitButton).toBeEnabled()

// Click
fireEvent.click(submitButton)
fireEvent.click(submitButton!)
expect(onSubmit).toHaveBeenCalledTimes(1)
expect(onSubmit.mock.lastCall[1]).toBe(true) // addEnhancedContext === true

Expand All @@ -66,11 +140,10 @@ describe('HumanMessageEditor', () => {
})

test('submit w/o context', async () => {
const { container, onSubmit } = renderWithMocks({
const { container, editor, onSubmit } = renderWithMocks({
initialEditorState: FILE_MENTION_EDITOR_STATE_FIXTURE,
__test_dontTemporarilyDisableSubmit: true,
})

const editor = container.querySelector<HTMLElement>('[data-lexical-editor="true"]')!
fireEvent.focus(editor)

// Click
Expand All @@ -91,9 +164,19 @@ describe('HumanMessageEditor', () => {
})
})

type EditorHTMLElement = HTMLDivElement & { dataset: { lexicalEditor: 'true' } }

function typeInEditor(editor: EditorHTMLElement, text: string): void {
fireEvent.focus(editor)
fireEvent.click(editor)
fireEvent.input(editor, { data: text })
}

function renderWithMocks(props: Partial<ComponentProps<typeof HumanMessageEditor>>): {
container: HTMLElement
submitButton: HTMLElement
editor: EditorHTMLElement
mentionButton: HTMLElement | null
submitButton: HTMLElement | null
onChange: Mock
onSubmit: Mock
} {
Expand All @@ -105,18 +188,23 @@ function renderWithMocks(props: Partial<ComponentProps<typeof HumanMessageEditor
initialEditorState: FILE_MENTION_EDITOR_STATE_FIXTURE,
placeholder: 'my-placeholder',
isFirstMessage: true,
isPendingResponse: false,
isSent: false,
onChange,
onSubmit,
isEditorInitiallyFocused: false,
}

const { container } = render(<HumanMessageEditor {...DEFAULT_PROPS} {...props} />, {
wrapper: AppWrapper,
})
return {
container,
submitButton: screen.getByRole('button', { name: 'Send with automatic code context' }),
editor: container.querySelector<EditorHTMLElement>('[data-lexical-editor="true"]')!,
mentionButton: screen.queryByRole('button', { name: 'Add context', hidden: true }),
submitButton: screen.queryByRole('button', {
name: 'Send with automatic code context',
hidden: true,
}),
onChange,
onSubmit,
}
Expand Down
Loading

0 comments on commit b2f8f05

Please sign in to comment.