Skip to content

Commit 18de56b

Browse files
authored
Merge pull request #2304 from cardstack/cs-7952-command-button-state-to-indicate-when-command-is-being-ii
Show ApplyButton's preparing state when a command is being prepared
2 parents 7ced2c8 + 5d7a7a9 commit 18de56b

File tree

16 files changed

+494
-171
lines changed

16 files changed

+494
-171
lines changed

packages/host/app/components/ai-assistant/apply-button/index.gts

+90-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { eq } from '@cardstack/boxel-ui/helpers';
55
import { CheckMark, Exclamation } from '@cardstack/boxel-ui/icons';
66
import { setCssVar } from '@cardstack/boxel-ui/modifiers';
77

8-
export type ApplyButtonState = 'ready' | 'applying' | 'applied' | 'failed';
8+
export type ApplyButtonState =
9+
| 'ready'
10+
| 'applying'
11+
| 'applied'
12+
| 'failed'
13+
| 'preparing';
914

1015
interface Signature {
1116
Element: HTMLButtonElement | HTMLDivElement;
@@ -34,6 +39,19 @@ const AiAssistantApplyButton: TemplateOnlyComponent<Signature> = <template>
3439
<CheckMark width='16' height='16' />
3540
{{else if (eq @state 'failed')}}
3641
<Exclamation width='16' height='16' />
42+
{{else if (eq @state 'preparing')}}
43+
<BoxelButton
44+
@kind='secondary-dark'
45+
@size='small'
46+
class='apply-button'
47+
tabindex='-1'
48+
disabled
49+
{{setCssVar boxel-button-text-color='var(--boxel-200)'}}
50+
data-test-apply-state='preparing'
51+
...attributes
52+
>
53+
Working…
54+
</BoxelButton>
3755
{{/if}}
3856
</div>
3957
{{/if}}
@@ -67,7 +85,71 @@ const AiAssistantApplyButton: TemplateOnlyComponent<Signature> = <template>
6785
width: 58px;
6886
border-radius: 100px;
6987
}
70-
.state-indicator:not(.applying) {
88+
89+
.state-indicator.preparing {
90+
width: 78px;
91+
padding: 1px;
92+
border-radius: 100px;
93+
}
94+
.state-indicator.preparing .apply-button {
95+
border: 0;
96+
min-width: 74px;
97+
}
98+
.state-indicator.preparing .apply-button:disabled {
99+
opacity: 1;
100+
}
101+
.state-indicator.preparing .apply-button:focus {
102+
--boxel-button-color: inherit;
103+
filter: none;
104+
cursor: not-allowed;
105+
}
106+
.state-indicator.preparing::before {
107+
content: '';
108+
position: absolute;
109+
top: -105px;
110+
left: -55px;
111+
width: 250px;
112+
height: 250px;
113+
background: conic-gradient(
114+
#ffcc8f 0deg,
115+
#ff3966 45deg,
116+
#ff309e 90deg,
117+
#aa1dc9 135deg,
118+
#d7fad6 180deg,
119+
#5fdfea 225deg,
120+
#3d83f2 270deg,
121+
#5145e8 315deg,
122+
#ffcc8f 360deg
123+
);
124+
z-index: -1;
125+
animation: spin 4s infinite linear;
126+
}
127+
128+
.state-indicator.preparing::after {
129+
content: '';
130+
position: absolute;
131+
top: 1px;
132+
left: 1px;
133+
right: 1px;
134+
bottom: 1px;
135+
background: var(--ai-bot-message-background-color);
136+
border-radius: inherit;
137+
z-index: -1;
138+
}
139+
140+
.state-indicator.preparing {
141+
position: relative;
142+
display: inline-block;
143+
border-radius: 3rem;
144+
color: white;
145+
background: var(--boxel-700);
146+
border: none;
147+
cursor: pointer;
148+
z-index: 1;
149+
overflow: hidden;
150+
}
151+
152+
.state-indicator:not(.applying):not(.preparing) {
71153
width: 1.5rem;
72154
aspect-ratio: 1;
73155
border-radius: 50%;
@@ -77,6 +159,12 @@ const AiAssistantApplyButton: TemplateOnlyComponent<Signature> = <template>
77159
background-color: var(--boxel-error-200);
78160
border-color: var(--boxel-error-200);
79161
}
162+
163+
@keyframes spin {
164+
to {
165+
transform: rotate(360deg);
166+
}
167+
}
80168
</style>
81169
</template>;
82170

packages/host/app/components/ai-assistant/apply-button/usage.gts

+15-8
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import FreestyleUsage from 'ember-freestyle/components/freestyle/usage';
88

99
import ApplyButton, { type ApplyButtonState } from './index';
1010

11+
let noop = () => {};
12+
1113
export default class AiAssistantApplyButtonUsage extends Component {
1214
@tracked state: ApplyButtonState = 'ready';
1315

14-
@action handleApplyButtonClick() {
16+
@action cycleState() {
1517
switch (this.state) {
1618
case 'ready':
1719
this.state = 'applying';
@@ -23,29 +25,34 @@ export default class AiAssistantApplyButtonUsage extends Component {
2325
this.state = 'failed';
2426
break;
2527
case 'failed':
28+
this.state = 'preparing';
29+
break;
30+
case 'preparing':
2631
this.state = 'ready';
2732
break;
2833
}
2934
}
3035
<template>
36+
{{! template-lint-disable no-inline-styles no-invalid-interactive }}
3137
<FreestyleUsage @name='AiAssistant::ApplyButton'>
3238
<:description>
3339
Displays button for applying change proposed by AI Assistant. Includes
34-
ready, applying, applied and failed states.
40+
ready, applying, applied, failed, and preparing states.
3541
</:description>
3642
<:example>
37-
<div class='example-container'>
38-
<ApplyButton
39-
@state={{this.state}}
40-
{{on 'click' this.handleApplyButtonClick}}
41-
/>
43+
<div
44+
class='example-container'
45+
style='--ai-bot-message-background-color: #3b394b;'
46+
{{on 'click' this.cycleState}}
47+
>
48+
<ApplyButton @state={{this.state}} {{on 'click' noop}} />
4249
</div>
4350
</:example>
4451
<:api as |Args|>
4552
<Args.String
4653
@name='state'
4754
@value={{this.state}}
48-
@options={{array 'ready' 'applying' 'applied' 'failed'}}
55+
@options={{array 'ready' 'applying' 'applied' 'failed' 'preparing'}}
4956
@description='Button state'
5057
@onInput={{fn (mut this.state)}}
5158
/>

packages/host/app/components/ai-assistant/panel.gts

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { service } from '@ember/service';
77
import Component from '@glimmer/component';
88
import { tracked, cached } from '@glimmer/tracking';
99

10-
import { restartableTask, timeout } from 'ember-concurrency';
10+
import { allSettled, restartableTask, timeout } from 'ember-concurrency';
1111
import { Velcro } from 'ember-velcro';
1212
import window from 'ember-window-mock';
1313

@@ -446,9 +446,7 @@ export default class AiAssistantPanel extends Component<Signature> {
446446

447447
private loadRoomsTask = restartableTask(async () => {
448448
await this.matrixService.flushAll;
449-
await Promise.allSettled(
450-
[...this.roomResources.values()].map((r) => r.loading),
451-
);
449+
await allSettled([...this.roomResources.values()].map((r) => r.processing));
452450
await this.enterRoomInitially();
453451
});
454452

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { TemplateOnlyComponent } from '@ember/component/template-only';
2+
3+
import ApplyButton from '../ai-assistant/apply-button';
4+
5+
interface Signature {
6+
Element: HTMLDivElement;
7+
}
8+
9+
const RoomMessageCommand: TemplateOnlyComponent<Signature> = <template>
10+
<div ...attributes>
11+
<div class='command-button-bar'>
12+
<ApplyButton @state='preparing' data-test-command-apply='preparing' />
13+
</div>
14+
</div>
15+
16+
<style scoped>
17+
.command-button-bar {
18+
display: flex;
19+
justify-content: flex-end;
20+
gap: var(--boxel-sp-xs);
21+
margin-top: var(--boxel-sp);
22+
}
23+
</style>
24+
</template>;
25+
26+
export default RoomMessageCommand;

packages/host/app/components/matrix/room-message-command.gts

+74-68
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
CardHeader,
1818
} from '@cardstack/boxel-ui/components';
1919

20-
import { MenuItem, bool, cn } from '@cardstack/boxel-ui/helpers';
20+
import { MenuItem, bool, cn, eq, not } from '@cardstack/boxel-ui/helpers';
2121
import { ArrowLeft } from '@cardstack/boxel-ui/icons';
2222

2323
import { cardTypeDisplayName, cardTypeIcon } from '@cardstack/runtime-common';
@@ -42,6 +42,8 @@ import { type ApplyButtonState } from '../ai-assistant/apply-button';
4242
import CodeBlock from '../ai-assistant/code-block';
4343
import Preview from '../preview';
4444

45+
import PreparingRoomMessageCommand from './preparing-room-message-command';
46+
4547
interface Signature {
4648
Element: HTMLDivElement;
4749
Args: {
@@ -51,6 +53,7 @@ interface Signature {
5153
runCommand: () => void;
5254
isError?: boolean;
5355
isPending?: boolean;
56+
isStreaming: boolean;
5457
monacoSDK: MonacoSDK;
5558
};
5659
}
@@ -166,78 +169,81 @@ export default class RoomMessageCommand extends Component<Signature> {
166169
{{#if @messageCommand.description}}
167170
<div class='command-description'>{{@messageCommand.description}}</div>
168171
{{/if}}
169-
<div
170-
class='command-button-bar'
171-
{{! In test, if we change this isIdle check to the task running locally on this component, it will fail because roomMessages get destroyed during re-indexing.
172-
Since services are long-lived so it we will not have this issue. I think this will go away when we convert our room field into a room component }}
173-
{{! TODO: Convert to non-EC async method after fixing CS-6987 }}
174-
data-test-command-card-idle={{this.commandService.run.isIdle}}
175-
>
176-
<Button
177-
class='view-code-button'
178-
{{on 'click' this.toggleViewCode}}
179-
@kind={{if this.isDisplayingCode 'primary-dark' 'secondary-dark'}}
180-
@size='extra-small'
181-
data-test-view-code-button
182-
>
183-
{{if this.isDisplayingCode 'Hide Code' 'View Code'}}
184-
</Button>
185-
<ApplyButton
186-
@state={{this.applyButtonState}}
187-
{{on 'click' @runCommand}}
188-
data-test-command-apply={{this.applyButtonState}}
189-
/>
190-
</div>
191-
{{#if this.isDisplayingCode}}
192-
<CodeBlock
193-
{{this.scrollBottomIntoView}}
194-
@monacoSDK={{@monacoSDK}}
195-
@code={{this.previewCommandCode}}
196-
@language='json'
197-
as |codeBlock|
172+
{{#if @isStreaming}}
173+
<PreparingRoomMessageCommand />
174+
{{else}}
175+
<div
176+
class='command-button-bar'
177+
data-test-command-card-idle={{not
178+
(eq @messageCommand.status 'applying')
179+
}}
198180
>
199-
<codeBlock.actions as |actions|>
200-
<actions.copyCode />
201-
</codeBlock.actions>
202-
<codeBlock.editor />
203-
</CodeBlock>
204-
{{/if}}
205-
{{#if this.failedCommandState}}
206-
<div class='failed-command-result'>
207-
<span class='failed-command-text'>
208-
{{this.failedCommandState.message}}
209-
</span>
210181
<Button
211-
{{on 'click' @runCommand}}
212-
class='retry-button'
213-
@size='small'
214-
@kind='secondary-dark'
215-
data-test-retry-command-button
182+
class='view-code-button'
183+
{{on 'click' this.toggleViewCode}}
184+
@kind={{if this.isDisplayingCode 'primary-dark' 'secondary-dark'}}
185+
@size='extra-small'
186+
data-test-view-code-button
216187
>
217-
Retry
188+
{{if this.isDisplayingCode 'Hide Code' 'View Code'}}
218189
</Button>
219-
</div>
220-
{{/if}}
221-
{{#if this.commandResultCard.card}}
222-
<CardContainer
223-
@displayBoundaries={{false}}
224-
class='command-result-card-preview'
225-
data-test-command-result-container
226-
>
227-
<CardHeader
228-
@cardTypeDisplayName={{this.headerTitle}}
229-
@cardTypeIcon={{cardTypeIcon this.commandResultCard.card}}
230-
@moreOptionsMenuItems={{this.moreOptionsMenuItems}}
231-
class='header'
232-
data-test-command-result-header
233-
/>
234-
<Preview
235-
@card={{this.commandResultCard.card}}
236-
@format='embedded'
237-
@displayContainer={{false}}
238-
data-test-boxel-command-result
190+
<ApplyButton
191+
@state={{this.applyButtonState}}
192+
{{on 'click' @runCommand}}
193+
data-test-command-apply={{this.applyButtonState}}
239194
/>
240-
</CardContainer>
195+
</div>
196+
{{#if this.isDisplayingCode}}
197+
<CodeBlock
198+
{{this.scrollBottomIntoView}}
199+
@monacoSDK={{@monacoSDK}}
200+
@code={{this.previewCommandCode}}
201+
@language='json'
202+
as |codeBlock|
203+
>
204+
<codeBlock.actions as |actions|>
205+
<actions.copyCode />
206+
</codeBlock.actions>
207+
<codeBlock.editor />
208+
</CodeBlock>
209+
{{/if}}
210+
{{#if this.failedCommandState}}
211+
<div class='failed-command-result'>
212+
<span class='failed-command-text'>
213+
{{this.failedCommandState.message}}
214+
</span>
215+
<Button
216+
{{on 'click' @runCommand}}
217+
class='retry-button'
218+
@size='small'
219+
@kind='secondary-dark'
220+
data-test-retry-command-button
221+
>
222+
Retry
223+
</Button>
224+
</div>
225+
{{/if}}
226+
{{#if this.commandResultCard.card}}
227+
<CardContainer
228+
@displayBoundaries={{false}}
229+
class='command-result-card-preview'
230+
data-test-command-result-container
231+
>
232+
<CardHeader
233+
@cardTypeDisplayName={{this.headerTitle}}
234+
@cardTypeIcon={{cardTypeIcon this.commandResultCard.card}}
235+
@moreOptionsMenuItems={{this.moreOptionsMenuItems}}
236+
class='header'
237+
data-test-command-result-header
238+
/>
239+
<Preview
240+
@card={{this.commandResultCard.card}}
241+
@format='embedded'
242+
@displayContainer={{false}}
243+
data-test-boxel-command-result
244+
/>
245+
</CardContainer>
246+
{{/if}}
241247
{{/if}}
242248
</div>
243249

0 commit comments

Comments
 (0)