Skip to content

Commit c1c9bb7

Browse files
committed
Show ApplyButton's preparing state when a command is being prepared
1 parent 040d46f commit c1c9bb7

File tree

8 files changed

+397
-97
lines changed

8 files changed

+397
-97
lines changed

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

+86-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,18 @@ 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+
{{setCssVar boxel-button-text-color='var(--boxel-200)'}}
49+
data-test-apply-state='preparing'
50+
...attributes
51+
>
52+
Working…
53+
</BoxelButton>
3754
{{/if}}
3855
</div>
3956
{{/if}}
@@ -67,7 +84,68 @@ const AiAssistantApplyButton: TemplateOnlyComponent<Signature> = <template>
6784
width: 58px;
6885
border-radius: 100px;
6986
}
70-
.state-indicator:not(.applying) {
87+
88+
.state-indicator.preparing {
89+
width: 78px;
90+
padding: 1px;
91+
border-radius: 100px;
92+
}
93+
.state-indicator.preparing .apply-button {
94+
border: 0;
95+
min-width: 74px;
96+
}
97+
.state-indicator.preparing .apply-button:focus {
98+
--boxel-button-color: inherit;
99+
filter: none;
100+
cursor: not-allowed;
101+
}
102+
.state-indicator.preparing::before {
103+
content: '';
104+
position: absolute;
105+
top: -105px;
106+
left: -55px;
107+
width: 250px;
108+
height: 250px;
109+
background: conic-gradient(
110+
#ffcc8f 0deg,
111+
#ff3966 45deg,
112+
#ff309e 90deg,
113+
#aa1dc9 135deg,
114+
#d7fad6 180deg,
115+
#5fdfea 225deg,
116+
#3d83f2 270deg,
117+
#5145e8 315deg,
118+
#ffcc8f 360deg
119+
);
120+
z-index: -1;
121+
animation: spin 4s infinite linear;
122+
}
123+
124+
.state-indicator.preparing::after {
125+
content: '';
126+
position: absolute;
127+
top: 1px;
128+
left: 1px;
129+
right: 1px;
130+
bottom: 1px;
131+
background: var(--ai-bot-message-background-color);
132+
border-radius: inherit;
133+
z-index: -1;
134+
}
135+
136+
.state-indicator.preparing {
137+
position: relative;
138+
display: inline-block;
139+
border-radius: 3rem;
140+
color: white;
141+
background: var(--boxel-700);
142+
border: none;
143+
cursor: pointer;
144+
z-index: 1;
145+
overflow: hidden;
146+
}
147+
148+
.state-indicator:not(.applying):not(.preparing) {
71149
width: 1.5rem;
72150
aspect-ratio: 1;
73151
border-radius: 50%;
@@ -77,6 +155,12 @@ const AiAssistantApplyButton: TemplateOnlyComponent<Signature> = <template>
77155
background-color: var(--boxel-error-200);
78156
border-color: var(--boxel-error-200);
79157
}
158+
159+
@keyframes spin {
160+
to {
161+
transform: rotate(360deg);
162+
}
163+
}
80164
</style>
81165
</template>;
82166

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export default class AiAssistantApplyButtonUsage extends Component {
2323
this.state = 'failed';
2424
break;
2525
case 'failed':
26+
this.state = 'preparing';
27+
break;
28+
case 'preparing':
2629
this.state = 'ready';
2730
break;
2831
}
@@ -31,7 +34,7 @@ export default class AiAssistantApplyButtonUsage extends Component {
3134
<FreestyleUsage @name='AiAssistant::ApplyButton'>
3235
<:description>
3336
Displays button for applying change proposed by AI Assistant. Includes
34-
ready, applying, applied and failed states.
37+
ready, applying, applied, failed, and preparing states.
3538
</:description>
3639
<:example>
3740
<div class='example-container'>
@@ -45,7 +48,7 @@ export default class AiAssistantApplyButtonUsage extends Component {
4548
<Args.String
4649
@name='state'
4750
@value={{this.state}}
48-
@options={{array 'ready' 'applying' 'applied' 'failed'}}
51+
@options={{array 'ready' 'applying' 'applied' 'failed' 'preparing'}}
4952
@description='Button state'
5053
@onInput={{fn (mut this.state)}}
5154
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
{{! template-lint-disable no-whitespace-for-layout }}
17+
{{! ignore the above error because ember-template-lint complains about the whitespace in the multi-line comment below }}
18+
<style scoped>
19+
.command-button-bar {
20+
display: flex;
21+
justify-content: flex-end;
22+
gap: var(--boxel-sp-xs);
23+
margin-top: var(--boxel-sp);
24+
}
25+
</style>
26+
</template>;
27+
28+
export default RoomMessageCommand;

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

+89-82
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import ApplyButton from '../ai-assistant/apply-button';
4444
import { type ApplyButtonState } from '../ai-assistant/apply-button';
4545
import Preview from '../preview';
4646

47+
import PreparingRoomMessageCommand from './preparing-room-message-command';
48+
4749
interface Signature {
4850
Element: HTMLDivElement;
4951
Args: {
@@ -53,6 +55,7 @@ interface Signature {
5355
runCommand: () => void;
5456
isError?: boolean;
5557
isPending?: boolean;
58+
isStreaming: boolean;
5659
monacoSDK: MonacoSDK;
5760
};
5861
}
@@ -193,97 +196,101 @@ export default class RoomMessageCommand extends Component<Signature> {
193196
{{#if @messageCommand.description}}
194197
<div class='command-description'>{{@messageCommand.description}}</div>
195198
{{/if}}
196-
<div
197-
class='command-button-bar'
198-
{{! 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.
199+
{{#if @isStreaming}}
200+
<PreparingRoomMessageCommand />
201+
{{else}}
202+
<div
203+
class='command-button-bar'
204+
{{! 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.
199205
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 }}
200-
{{! TODO: Convert to non-EC async method after fixing CS-6987 }}
201-
data-test-command-card-idle={{this.commandService.run.isIdle}}
202-
>
203-
<Button
204-
class='view-code-button'
205-
{{on 'click' this.toggleViewCode}}
206-
@kind={{if this.isDisplayingCode 'primary-dark' 'secondary-dark'}}
207-
@size='extra-small'
208-
data-test-view-code-button
206+
{{! TODO: Convert to non-EC async method after fixing CS-6987 }}
207+
data-test-command-card-idle={{this.commandService.run.isIdle}}
209208
>
210-
{{if this.isDisplayingCode 'Hide Code' 'View Code'}}
211-
</Button>
212-
<ApplyButton
213-
@state={{this.applyButtonState}}
214-
{{on 'click' @runCommand}}
215-
data-test-command-apply={{this.applyButtonState}}
216-
/>
217-
</div>
218-
{{#if this.isDisplayingCode}}
219-
<div class='preview-code'>
220209
<Button
221-
class='code-copy-button'
222-
@kind='text-only'
210+
class='view-code-button'
211+
{{on 'click' this.toggleViewCode}}
212+
@kind={{if this.isDisplayingCode 'primary-dark' 'secondary-dark'}}
223213
@size='extra-small'
224-
{{on 'click' this.copyToClipboard}}
225-
data-test-copy-code
214+
data-test-view-code-button
226215
>
227-
<CopyIcon
228-
width='16'
229-
height='16'
230-
role='presentation'
231-
aria-hidden='true'
232-
/>
233-
<span class='copy-text'>Copy to clipboard</span>
216+
{{if this.isDisplayingCode 'Hide Code' 'View Code'}}
234217
</Button>
235-
<div
236-
class='monaco-container'
237-
{{this.scrollBottomIntoView}}
238-
{{monacoModifier
239-
content=this.previewCommandCode
240-
contentChanged=undefined
241-
monacoSDK=@monacoSDK
242-
language='json'
243-
readOnly=true
244-
editorDisplayOptions=this.editorDisplayOptions
245-
}}
246-
data-test-editor
247-
data-test-percy-hide
218+
<ApplyButton
219+
@state={{this.applyButtonState}}
220+
{{on 'click' @runCommand}}
221+
data-test-command-apply={{this.applyButtonState}}
248222
/>
249223
</div>
250-
{{/if}}
251-
{{#if this.failedCommandState}}
252-
<div class='failed-command-result'>
253-
<span class='failed-command-text'>
254-
{{this.failedCommandState.message}}
255-
</span>
256-
<Button
257-
{{on 'click' @runCommand}}
258-
class='retry-button'
259-
@size='small'
260-
@kind='secondary-dark'
261-
data-test-retry-command-button
224+
{{#if this.isDisplayingCode}}
225+
<div class='preview-code'>
226+
<Button
227+
class='code-copy-button'
228+
@kind='text-only'
229+
@size='extra-small'
230+
{{on 'click' this.copyToClipboard}}
231+
data-test-copy-code
232+
>
233+
<CopyIcon
234+
width='16'
235+
height='16'
236+
role='presentation'
237+
aria-hidden='true'
238+
/>
239+
<span class='copy-text'>Copy to clipboard</span>
240+
</Button>
241+
<div
242+
class='monaco-container'
243+
{{this.scrollBottomIntoView}}
244+
{{monacoModifier
245+
content=this.previewCommandCode
246+
contentChanged=undefined
247+
monacoSDK=@monacoSDK
248+
language='json'
249+
readOnly=true
250+
editorDisplayOptions=this.editorDisplayOptions
251+
}}
252+
data-test-editor
253+
data-test-percy-hide
254+
/>
255+
</div>
256+
{{/if}}
257+
{{#if this.failedCommandState}}
258+
<div class='failed-command-result'>
259+
<span class='failed-command-text'>
260+
{{this.failedCommandState.message}}
261+
</span>
262+
<Button
263+
{{on 'click' @runCommand}}
264+
class='retry-button'
265+
@size='small'
266+
@kind='secondary-dark'
267+
data-test-retry-command-button
268+
>
269+
Retry
270+
</Button>
271+
</div>
272+
{{/if}}
273+
{{#if this.commandResultCard.card}}
274+
<CardContainer
275+
@displayBoundaries={{false}}
276+
class='command-result-card-preview'
277+
data-test-command-result-container
262278
>
263-
Retry
264-
</Button>
265-
</div>
266-
{{/if}}
267-
{{#if this.commandResultCard.card}}
268-
<CardContainer
269-
@displayBoundaries={{false}}
270-
class='command-result-card-preview'
271-
data-test-command-result-container
272-
>
273-
<CardHeader
274-
@cardTypeDisplayName={{this.headerTitle}}
275-
@cardTypeIcon={{cardTypeIcon this.commandResultCard.card}}
276-
@moreOptionsMenuItems={{this.moreOptionsMenuItems}}
277-
class='header'
278-
data-test-command-result-header
279-
/>
280-
<Preview
281-
@card={{this.commandResultCard.card}}
282-
@format='embedded'
283-
@displayContainer={{false}}
284-
data-test-boxel-command-result
285-
/>
286-
</CardContainer>
279+
<CardHeader
280+
@cardTypeDisplayName={{this.headerTitle}}
281+
@cardTypeIcon={{cardTypeIcon this.commandResultCard.card}}
282+
@moreOptionsMenuItems={{this.moreOptionsMenuItems}}
283+
class='header'
284+
data-test-command-result-header
285+
/>
286+
<Preview
287+
@card={{this.commandResultCard.card}}
288+
@format='embedded'
289+
@displayContainer={{false}}
290+
data-test-boxel-command-result
291+
/>
292+
</CardContainer>
293+
{{/if}}
287294
{{/if}}
288295
</div>
289296

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

+1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export default class RoomMessage extends Component<Signature> {
136136
@isPending={{@isPending}}
137137
@monacoSDK={{@monacoSDK}}
138138
@isError={{bool this.errorMessage}}
139+
@isStreaming={{@isStreaming}}
139140
/>
140141
{{/each}}
141142
</AiAssistantMessage>

0 commit comments

Comments
 (0)