Skip to content

Commit a8456f7

Browse files
authored
feat: Add multiline input to AI chat (#191)
1 parent 1477831 commit a8456f7

File tree

7 files changed

+186
-27
lines changed

7 files changed

+186
-27
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
# smoot-design
1+
# @mitodl/smoot-design
22

33
Design system components for MITODL Projects
44

5+
![NPM Version](https://img.shields.io/npm/v/@mitodl/smoot-design)
6+
7+
![NPM Last Update](https://img.shields.io/npm/last-update/@mitodl/smoot-design?label=npm+last+update)
8+
9+
![NPM Unpacked Size](https://img.shields.io/npm/unpacked-size/@mitodl/smoot-design)
10+
11+
![GitHub branch status](https://img.shields.io/github/checks-status/mitodl/smoot-design/main)
12+
513
## Development and Release
614

715
All PR titles and commits to `main` should use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) format. During release, the types of commits included since the last release inform what sort of version bump should be made. For example, bugfixes yield a new patch version, whereas breaking changes trigger a major version bump.

src/bundles/AiDrawer/AiDrawerManager.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ describe("AiDrawerManager", () => {
423423
},
424424
)
425425

426-
await user.keyboard("{Escape}")
426+
await user.click(screen.getByRole("button", { name: "Close" }))
427427
assertTrackingEvent({
428428
event_type: `${eventPrefix}.${TrackingEventType.Close}`,
429429
event_data: { blockUsageKey },

src/components/AiChat/AiChat.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,13 +610,24 @@ const AiChatDisplay: FC<AiChatDisplayProps> = ({
610610
<Input
611611
ref={promptInputRef}
612612
fullWidth
613+
multiline
614+
maxRows={20}
613615
size="chat"
614616
className={classes.input}
615617
placeholder={placeholder}
616618
name="message"
617619
sx={{ flex: 1 }}
618620
value={input}
619621
onChange={handleInputChange}
622+
onKeyDown={(event) => {
623+
if (event.key === "Enter" && !event.shiftKey) {
624+
event.preventDefault()
625+
if (input.trim() && !isLoading) {
626+
const form = event.currentTarget.closest("form")
627+
form?.requestSubmit()
628+
}
629+
}
630+
}}
620631
inputProps={{
621632
"aria-label": "Ask a question",
622633
}}

src/components/AiChat/EntryScreen.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ const EntryScreen = ({
139139
<StyledInput
140140
fullWidth
141141
size="chat"
142+
multiline
143+
maxRows={20}
142144
name="prompt"
143145
onChange={onPromptChange}
144146
inputProps={{
@@ -150,6 +152,13 @@ const EntryScreen = ({
150152
</AdornmentButton>
151153
}
152154
responsive
155+
onKeyDown={(event) => {
156+
if (event.key === "Enter" && !event.shiftKey) {
157+
event.preventDefault()
158+
const form = event.currentTarget.closest("form")
159+
form?.requestSubmit()
160+
}
161+
}}
153162
/>
154163
<Starters>
155164
{conversationStarters?.map(({ content }, index) => (

src/components/Input/Input.stories.tsx

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import { fn } from "storybook/test"
1010
import { enumValues } from "../../story-utils"
1111
import Typography from "@mui/material/Typography"
1212

13+
const StatefulInput = (props: InputProps) => {
14+
const [value, setValue] = React.useState(props.value || "")
15+
16+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
17+
setValue(event.target.value)
18+
props.onChange?.(event)
19+
}
20+
21+
return <Input {...props} value={value} onChange={handleChange} />
22+
}
23+
1324
const SIZES = enumValues<NonNullable<InputProps["size"]>>({
1425
small: true,
1526
medium: true,
@@ -76,10 +87,10 @@ export const Sizes: Story = {
7687
render: (args) => {
7788
return (
7889
<Stack direction="row" gap={1}>
79-
<Input size="small" {...args} />
80-
<Input size="medium" {...args} />
81-
<Input size="large" {...args} />
82-
<Input size="hero" {...args} />
90+
<StatefulInput size="small" {...args} />
91+
<StatefulInput size="medium" {...args} />
92+
<StatefulInput size="large" {...args} />
93+
<StatefulInput size="hero" {...args} />
8394
</Stack>
8495
)
8596
},
@@ -112,7 +123,7 @@ export const Adornments: Story = {
112123
SIZES.map((size) => {
113124
return (
114125
<Grid size={{ xs: 3 }} key={`${i}-${size}`}>
115-
<Input {...args} size={size} {...props} />
126+
<StatefulInput {...args} size={size} {...props} />
116127
</Grid>
117128
)
118129
}),
@@ -126,6 +137,50 @@ export const Adornments: Story = {
126137
},
127138
}
128139

140+
export const Multiline: Story = {
141+
render: (args) => {
142+
return (
143+
<Stack direction="column" gap={1}>
144+
<StatefulInput
145+
size="small"
146+
{...args}
147+
multiline
148+
endAdornment={ADORNMENTS.SearchIcon}
149+
/>
150+
<StatefulInput
151+
size="medium"
152+
{...args}
153+
multiline
154+
endAdornment={ADORNMENTS.SearchIcon}
155+
/>
156+
<StatefulInput
157+
size="large"
158+
{...args}
159+
multiline
160+
endAdornment={ADORNMENTS.SearchIcon}
161+
/>
162+
<StatefulInput
163+
size="hero"
164+
{...args}
165+
multiline
166+
endAdornment={ADORNMENTS.SearchIcon}
167+
/>
168+
<StatefulInput
169+
size="chat"
170+
{...args}
171+
value={`Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
172+
173+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
174+
175+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`}
176+
multiline
177+
endAdornment={ADORNMENTS.SearchIcon}
178+
/>
179+
</Stack>
180+
)
181+
},
182+
}
183+
129184
const PageStyles = styled.div(`
130185
input {
131186
background-color: red;
@@ -159,19 +214,19 @@ export const StatesAndParentStyleResistance: Story = {
159214
<Typography>Placeholder</Typography>
160215
</Grid>
161216
<Grid size={{ xs: 8 }}>
162-
<Input {...args} value="" />
217+
<StatefulInput {...args} value="" />
163218
</Grid>
164219
<Grid size={{ xs: 4 }}>
165220
<Typography>Default</Typography>
166221
</Grid>
167222
<Grid size={{ xs: 8 }}>
168-
<Input {...args} />
223+
<StatefulInput {...args} />
169224
</Grid>
170225
<Grid size={{ xs: 4 }}>
171226
<Typography>Initially Focused</Typography>
172227
</Grid>
173228
<Grid size={{ xs: 8 }}>
174-
<Input
229+
<StatefulInput
175230
// This is a story just demonstrating the autofocus prop
176231
// eslint-disable-next-line jsx-a11y/no-autofocus
177232
autoFocus
@@ -182,19 +237,19 @@ export const StatesAndParentStyleResistance: Story = {
182237
<Typography>Error</Typography>
183238
</Grid>
184239
<Grid size={{ xs: 8 }}>
185-
<Input {...args} error />
240+
<StatefulInput {...args} error />
186241
</Grid>
187242
<Grid size={{ xs: 4 }}>
188243
<Typography>Disabled</Typography>
189244
</Grid>
190245
<Grid size={{ xs: 8 }}>
191-
<Input {...args} disabled />
246+
<StatefulInput {...args} disabled />
192247
</Grid>
193248
<Grid size={{ xs: 4 }}>
194249
<Typography>Password</Typography>
195250
</Grid>
196251
<Grid size={{ xs: 8 }}>
197-
<Input {...args} type="password" />
252+
<StatefulInput {...args} type="password" />
198253
</Grid>
199254
</Grid>
200255
</PageStyles>

src/components/Input/Input.tsx

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ const sizeStyles = ({ size, theme, multiline }: SizeStyleProps) =>
8383
!multiline && {
8484
height: "56px",
8585
},
86+
size === "chat" &&
87+
multiline && {
88+
"& textarea": {
89+
paddingTop: "16px",
90+
paddingBottom: "16px",
91+
},
92+
},
8693
size === "hero" &&
8794
!multiline && {
8895
height: "72px",
@@ -128,6 +135,51 @@ const sizeStyles = ({ size, theme, multiline }: SizeStyleProps) =>
128135
width: "72px",
129136
},
130137
},
138+
multiline && {
139+
".Mit-AdornmentButton": {
140+
alignSelf: "flex-end",
141+
},
142+
},
143+
(multiline && !size) ||
144+
(size === "small" && {
145+
".Mit-AdornmentButton": {
146+
padding: "8px",
147+
margin: "4px",
148+
borderRadius: "4px",
149+
},
150+
}),
151+
multiline &&
152+
size === "medium" && {
153+
".Mit-AdornmentButton": {
154+
padding: "10px",
155+
margin: "4px",
156+
borderRadius: "4px",
157+
},
158+
},
159+
multiline &&
160+
size === "large" && {
161+
".Mit-AdornmentButton": {
162+
padding: "12px",
163+
margin: "4px",
164+
borderRadius: "4px",
165+
},
166+
},
167+
multiline &&
168+
size === "hero" && {
169+
".Mit-AdornmentButton": {
170+
padding: "24px",
171+
margin: "8px",
172+
borderRadius: "4px",
173+
},
174+
},
175+
multiline &&
176+
size === "chat" && {
177+
".Mit-AdornmentButton": {
178+
padding: "10px",
179+
margin: "8px",
180+
borderRadius: "4px",
181+
},
182+
},
131183
])
132184

133185
/**
@@ -245,6 +297,7 @@ const AdornmentButtonStyled = styled.button(({ theme, disabled }) => ({
245297
flexShrink: 0,
246298
justifyContent: "center",
247299
alignItems: "center",
300+
alignSelf: "stretch",
248301
// background and border
249302
border: "none",
250303
background: "transparent",
@@ -270,7 +323,6 @@ const AdornmentButtonStyled = styled.button(({ theme, disabled }) => ({
270323
".MuiInputBase-root.Mui-disabled &": {
271324
color: "inherit",
272325
},
273-
height: "100%",
274326
}))
275327

276328
const noFocus: React.MouseEventHandler = (e) => e.preventDefault()

0 commit comments

Comments
 (0)