Skip to content

Commit bc51b97

Browse files
committed
Attach files to the input section
1 parent 2253db2 commit bc51b97

File tree

11 files changed

+150
-45
lines changed

11 files changed

+150
-45
lines changed

.github/workflows/pr-boxel-host.yml

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
AWS_REGION: us-east-1
6666
AWS_CLOUDFRONT_DISTRIBUTION: EU4RGLH4EOCHJ
6767
ENABLE_PLAYGROUND: true
68+
ENABLE_ATTACHING_FILES: true
6869
with:
6970
package: boxel-host
7071
environment: staging
@@ -96,6 +97,7 @@ jobs:
9697
AWS_REGION: us-east-1
9798
AWS_CLOUDFRONT_DISTRIBUTION: E2PZR9CIAW093B
9899
ENABLE_PLAYGROUND: true
100+
ENABLE_ATTACHING_FILES: true
99101
with:
100102
package: boxel-host
101103
environment: production
Loading

packages/boxel-ui/addon/src/icons.gts

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Card from './icons/card.gts';
1111
import CardbotLg from './icons/cardbot-lg.gts';
1212
import CaretDown from './icons/caret-down.gts';
1313
import CheckMark from './icons/check-mark.gts';
14+
import CodeFile from './icons/code-file.gts';
1415
import Copy from './icons/copy.gts';
1516
import DiagonalArrowLeftUp from './icons/diagonal-arrow-left-up.gts';
1617
import Download from './icons/download.gts';
@@ -71,6 +72,7 @@ export const ALL_ICON_COMPONENTS = [
7172
CardbotLg,
7273
CaretDown,
7374
CheckMark,
75+
CodeFile,
7476
Copy,
7577
DiagonalArrowLeftUp,
7678
Download,
@@ -132,6 +134,7 @@ export {
132134
CardbotLg,
133135
CaretDown,
134136
CheckMark,
137+
CodeFile,
135138
Copy,
136139
DiagonalArrowLeftUp,
137140
Download,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// This file is auto-generated by 'pnpm rebuild:icons'
2+
import type { TemplateOnlyComponent } from '@ember/component/template-only';
3+
4+
import type { Signature } from './types.ts';
5+
6+
const IconComponent: TemplateOnlyComponent<Signature> = <template>
7+
<svg
8+
xmlns='http://www.w3.org/2000/svg'
9+
fill='none'
10+
transform='scale(-1 1)'
11+
viewBox='0 0 24 24'
12+
...attributes
13+
><path
14+
fill='var(--icon-color, #000000)'
15+
fill-rule='evenodd'
16+
d='M9.293 1.293A1 1 0 0 1 10 1h8a3 3 0 0 1 3 3v5a1 1 0 1 1-2 0V4a1 1 0 0 0-1-1h-7v5a1 1 0 0 1-1 1H5v11a1 1 0 0 0 1 1h3a1 1 0 1 1 0 2H6a3 3 0 0 1-3-3V8a1 1 0 0 1 .293-.707l6-6ZM6.414 7H9V4.414L6.414 7Zm12.293 5.293 4 4a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414L20.586 17l-3.293-3.293a1 1 0 0 1 1.414-1.414Zm-4 1.414a1 1 0 0 0-1.414-1.414l-4 4a1 1 0 0 0 0 1.414l4 4a1 1 0 0 0 1.414-1.414L11.414 17l3.293-3.293Z'
17+
clip-rule='evenodd'
18+
/></svg>
19+
</template>;
20+
21+
// @ts-expect-error this is the only way to set a name on a Template Only Component currently
22+
IconComponent.name = 'CodeFile';
23+
export default IconComponent;

packages/host/app/components/ai-assistant/attachment-picker/index.gts

+44-28
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323

2424
import Pill from '@cardstack/host/components/pill';
2525

26+
import ENV from '@cardstack/host/config/environment';
27+
2628
import { type CardDef } from 'https://cardstack.com/base/card-api';
2729
import { type FileDef } from 'https://cardstack.com/base/file-api';
2830

@@ -44,36 +46,37 @@ interface Signature {
4446
};
4547
}
4648

49+
const isAttachingFilesEnabled = ENV.featureFlags?.ENABLE_ATTACHING_FILES;
4750
const MAX_ITEMS_TO_DISPLAY = 4;
4851

4952
export default class AiAssistantAttachmentPicker extends Component<Signature> {
5053
<template>
5154
<div class='item-picker'>
5255
{{#each this.itemsToDisplay as |item|}}
53-
{{#if (this.isCard item)}}
54-
{{#if (this.isAutoAttachedCard item)}}
55-
<Tooltip @placement='top'>
56-
<:trigger>
57-
<Pill
58-
@item={{item}}
59-
@isAutoAttached={{true}}
60-
@remove={{this.removeItem}}
61-
/>
62-
</:trigger>
63-
64-
<:content>
65-
{{#if (this.isAutoAttachedCard item)}}
66-
Topmost card is shared automatically
67-
{{/if}}
68-
</:content>
69-
</Tooltip>
70-
{{else}}
71-
<Pill
72-
@item={{item}}
73-
@isAutoAttached={{false}}
74-
@remove={{this.removeItem}}
75-
/>
76-
{{/if}}
56+
{{#if (this.isAutoAttached item)}}
57+
<Tooltip @placement='top'>
58+
<:trigger>
59+
<Pill
60+
@item={{item}}
61+
@isAutoAttached={{true}}
62+
@remove={{this.removeItem}}
63+
/>
64+
</:trigger>
65+
66+
<:content>
67+
{{#if (this.isAutoAttached item)}}
68+
Topmost
69+
{{if (this.isCard item) 'Card' 'File'}}
70+
is shared automatically
71+
{{/if}}
72+
</:content>
73+
</Tooltip>
74+
{{else}}
75+
<Pill
76+
@item={{item}}
77+
@isAutoAttached={{false}}
78+
@remove={{this.removeItem}}
79+
/>
7780
{{/if}}
7881
{{/each}}
7982
{{#if
@@ -91,7 +94,7 @@ export default class AiAssistantAttachmentPicker extends Component<Signature> {
9194
</BoxelPill>
9295
{{/if}}
9396
{{#if this.canDisplayAddButton}}
94-
{{#if (eq @submode 'code')}}
97+
{{#if (and (eq @submode 'code') isAttachingFilesEnabled)}}
9598
<AddButton
9699
class={{cn 'attach-button' icon-only=this.itemsToDisplay.length}}
97100
@variant='pill'
@@ -162,17 +165,30 @@ export default class AiAssistantAttachmentPicker extends Component<Signature> {
162165
this.areAllItemsDisplayed = !this.areAllItemsDisplayed;
163166
}
164167

165-
isCard = (item: CardDef | FileDef): item is CardDef => {
168+
private isCard = (item: CardDef | FileDef): item is CardDef => {
166169
return isCardInstance(item);
167170
};
168171

169-
isAutoAttachedCard = (card: CardDef) => {
172+
private isAutoAttachedCard = (card: CardDef) => {
170173
if (this.args.autoAttachedCards === undefined) {
171174
return false;
172175
}
173176
return this.args.autoAttachedCards.has(card);
174177
};
175178

179+
private isAutoAttachedFile = (file: FileDef) => {
180+
if (this.args.autoAttachedFiles === undefined) {
181+
return false;
182+
}
183+
return this.args.autoAttachedFiles.includes(file);
184+
};
185+
186+
private isAutoAttached = (item: CardDef | FileDef) => {
187+
return this.isCard(item)
188+
? this.isAutoAttachedCard(item)
189+
: this.isAutoAttachedFile(item);
190+
};
191+
176192
private get items() {
177193
let cards = this.args.cardsToAttach ?? [];
178194
let files = this.args.filesToAttach ?? [];
@@ -231,7 +247,7 @@ export default class AiAssistantAttachmentPicker extends Component<Signature> {
231247

232248
@action
233249
private removeItem(item: CardDef | FileDef) {
234-
if (cardApi(item)) {
250+
if (isCardInstance(item)) {
235251
this.args.removeCard(item);
236252
} else {
237253
this.args.removeFile(item);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ export default class Room extends Component<Signature> {
635635
// );
636636
} else {
637637
const fileIndex = this.filesToAttach?.findIndex(
638-
(f) => f.sourceUrl === f.sourceUrl,
638+
(f) => f.sourceUrl === file.sourceUrl,
639639
);
640640
if (fileIndex != undefined && fileIndex !== -1) {
641641
if (this.filesToAttach !== undefined) {

packages/host/app/components/operator-mode/attach-file-modal.gts

+16-8
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,22 @@ import {
1616
BoxelSelect,
1717
} from '@cardstack/boxel-ui/components';
1818

19-
import { Deferred, type LocalPath } from '@cardstack/runtime-common';
19+
import {
20+
Deferred,
21+
RealmPaths,
22+
type LocalPath,
23+
} from '@cardstack/runtime-common';
2024

2125
import ModalContainer from '@cardstack/host/components/modal-container';
2226

27+
import MatrixService from '@cardstack/host/services/matrix-service';
2328
import OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service';
2429

2530
import type RealmService from '@cardstack/host/services/realm';
2631

2732
import { type FileDef } from 'https://cardstack.com/base/file-api';
2833

2934
import FileTree from '../editor/file-tree';
30-
import MatrixService from '@cardstack/host/services/matrix-service';
3135

3236
interface Signature {
3337
Args: {};
@@ -51,7 +55,7 @@ export default class AttachFileModal extends Component<Signature> {
5155
}
5256

5357
// public API
54-
async chooseFile<T>(): Promise<undefined | T> {
58+
async chooseFile<T extends FileDef>(): Promise<undefined | T> {
5559
this.deferred = new Deferred();
5660
let defaultRealm = this.knownRealms.find(
5761
(r) =>
@@ -69,11 +73,12 @@ export default class AttachFileModal extends Component<Signature> {
6973

7074
@action
7175
private pick(path: LocalPath | undefined) {
72-
if (this.deferred && path) {
76+
if (this.deferred && this.selectedRealm && path) {
77+
let fileURL = new RealmPaths(this.selectedRealm.url).fileURL(path);
7378
let file = this.matrixService.fileAPI.createFileDef({
74-
url: path,
75-
sourceUrl: path,
76-
name: path.split('/').pop()!,
79+
url: fileURL.toString(),
80+
sourceUrl: fileURL.toString(),
81+
name: fileURL.toString().split('/').pop()!,
7782
});
7883
this.deferred.fulfill(file);
7984
}
@@ -200,7 +205,10 @@ export default class AttachFileModal extends Component<Signature> {
200205
data-test-attach-file-modal-realm-chooser
201206
as |item|
202207
>
203-
<div class='realm-chooser__options'>
208+
<div
209+
class='realm-chooser__options'
210+
data-test-attach-file-modal-realm-option={{item.info.name}}
211+
>
204212
<img src={{item.info.iconURL}} alt='realm icon' />
205213
<span>{{item.info.name}}</span>
206214
</div>

packages/host/app/components/pill.gts

+15-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
RealmIcon,
1010
Switch,
1111
} from '@cardstack/boxel-ui/components';
12-
import { cn } from '@cardstack/boxel-ui/helpers';
13-
import { IconX } from '@cardstack/boxel-ui/icons';
12+
import { cn, cssVar } from '@cardstack/boxel-ui/helpers';
13+
import { IconX, CodeFile } from '@cardstack/boxel-ui/icons';
14+
15+
import { isCardInstance } from '@cardstack/runtime-common';
1416

1517
import { type CardDef } from 'https://cardstack.com/base/card-api';
1618

@@ -41,13 +43,13 @@ export default class Pill extends Component<PillSignature> {
4143
}
4244

4345
get id() {
44-
return 'id' in this.args.item
46+
return isCardInstance(this.args.item)
4547
? this.args.item.id
4648
: this.args.item.sourceUrl;
4749
}
4850

4951
get title() {
50-
return 'title' in this.args.item
52+
return isCardInstance(this.args.item)
5153
? this.args.item.title
5254
: this.args.item.name;
5355
}
@@ -64,7 +66,15 @@ export default class Pill extends Component<PillSignature> {
6466
...attributes
6567
>
6668
<:iconLeft>
67-
<RealmIcon @realmInfo={{this.realm.info this.id}} />
69+
{{#if (isCardInstance @item)}}
70+
<RealmIcon @realmInfo={{this.realm.info this.id}} />
71+
{{else}}
72+
<CodeFile
73+
width='16px'
74+
height='16px'
75+
style={{cssVar icon-color='#0031ff'}}
76+
/>
77+
{{/if}}
6878
</:iconLeft>
6979
<:default>
7080
<div class='pill-content' title={{this.title}}>

packages/host/app/config/environment.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ declare const config: {
3030
stripePaymentLink: string;
3131
featureFlags?: {
3232
ENABLE_PLAYGROUND: boolean;
33+
ENABLE_ATTACHING_FILES: boolean;
3334
};
3435
};

packages/host/config/environment.js

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ module.exports = function (environment) {
4343
process.env.RESOLVED_BASE_REALM_URL || 'http://localhost:4201/base/',
4444
featureFlags: {
4545
ENABLE_PLAYGROUND: process.env.ENABLE_PLAYGROUND || false,
46+
ENABLE_ATTACHING_FILES: process.env.ENABLE_ATTACHING_FILES || false,
4647
},
4748
};
4849

@@ -54,6 +55,7 @@ module.exports = function (environment) {
5455
// ENV.APP.LOG_VIEW_LOOKUPS = true;
5556
ENV.featureFlags = {
5657
ENABLE_PLAYGROUND: true,
58+
ENABLE_ATTACHING_FILES: true,
5759
};
5860
}
5961

packages/host/tests/acceptance/ai-assistant-test.gts

+34-3
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,43 @@ module('Acceptance | AI Assistant tests', function (hooks) {
368368
],
369369
});
370370
await click('[data-test-open-ai-assistant]');
371-
assert.dom('[data-test-choose-card-btn]').hasText('Attach File');
371+
assert.dom('[data-test-choose-file-btn]').hasText('Attach File');
372372

373-
await click('[data-test-choose-card-btn]');
373+
await click('[data-test-choose-file-btn]');
374374
assert.dom('[data-test-attach-file-modal]').exists();
375+
assert.dom('[data-test-file="pet.gts"]').exists();
375376

377+
// Change realm
376378
await click('[data-test-attach-file-modal-realm-chooser]');
377-
assert.dom(`[data-test-file="${testRealmURL}index"]`).exists();
379+
await click('[data-test-attach-file-modal-realm-option="Base Workspace"]');
380+
assert.dom('[data-test-file="boolean.gts"]').exists();
381+
382+
await click('[data-test-attach-file-modal-realm-chooser]');
383+
await click(
384+
'[data-test-attach-file-modal-realm-option="Test Workspace B"]',
385+
);
386+
387+
// Add attachment item
388+
await click('[data-test-file="person.gts"]');
389+
await click('[data-test-attach-file-modal-add-button]');
390+
assert.dom('[data-test-attached-item]').exists({ count: 1 });
391+
assert.dom('[data-test-attached-item]').hasText('person.gts');
392+
// Add attachment item
393+
await click('[data-test-choose-file-btn]');
394+
await click('[data-test-file="pet.gts"]');
395+
await click('[data-test-attach-file-modal-add-button]');
396+
assert.dom('[data-test-attached-item]').exists({ count: 2 });
397+
assert
398+
.dom(`[data-test-attached-item="${testRealmURL}person.gts"]`)
399+
.hasText('person.gts');
400+
assert
401+
.dom(`[data-test-attached-item="${testRealmURL}pet.gts"]`)
402+
.hasText('pet.gts');
403+
404+
// Add remove attachment item
405+
await click(
406+
`[data-test-attached-item="${testRealmURL}person.gts"] [data-test-remove-item-btn]`,
407+
);
408+
assert.dom('[data-test-attached-item]').hasText('pet.gts');
378409
});
379410
});

0 commit comments

Comments
 (0)