Skip to content

Commit

Permalink
Merge branch 'main' into CS-7926-link-crm-cards-to-crm-app
Browse files Browse the repository at this point in the history
  • Loading branch information
richardhjtan committed Feb 13, 2025
2 parents e5080bc + 9fe7e54 commit 4e07232
Show file tree
Hide file tree
Showing 20 changed files with 1,003 additions and 425 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/pr-boxel-host.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
AWS_S3_BUCKET: boxel-host-preview.stack.cards
AWS_REGION: us-east-1
AWS_CLOUDFRONT_DISTRIBUTION: EU4RGLH4EOCHJ
ENABLE_PLAYGROUND: true
with:
package: boxel-host
environment: staging
Expand Down Expand Up @@ -94,6 +95,7 @@ jobs:
AWS_S3_BUCKET: boxel-host-preview.boxel.ai
AWS_REGION: us-east-1
AWS_CLOUDFRONT_DISTRIBUTION: E2PZR9CIAW093B
ENABLE_PLAYGROUND: true
with:
package: boxel-host
environment: production
39 changes: 39 additions & 0 deletions packages/base/file-api.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import FileIcon from '@cardstack/boxel-icons/file';
import {
BaseDef,
BaseDefComponent,
Component,
StringField,
contains,
field,
} from './card-api';

class View extends Component<typeof FileDef> {
<template>
{{@model.name}}
</template>
}

export class FileDef extends BaseDef {
static displayName = 'File';
static icon = FileIcon;

@field sourceUrl = contains(StringField);
@field url = contains(StringField);
@field name = contains(StringField);
@field type = contains(StringField);

static embedded: BaseDefComponent = View;
static fitted: BaseDefComponent = View;
static isolated: BaseDefComponent = View;
static atom: BaseDefComponent = View;

serialize() {
return {
sourceUrl: this.sourceUrl,
url: this.url,
name: this.name,
type: this.type,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const LoadingIndicator: TemplateOnlyComponent<Signature> = <template>
--boxel-loading-indicator-size,
var(--boxel-icon-sm)
);
display: inline-block;
width: var(--loading-indicator-size);
height: var(--loading-indicator-size);
flex-shrink: 0;
Expand Down
22 changes: 21 additions & 1 deletion packages/experiments-realm/crm/account.gts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ export class IsolatedTemplate extends Component<typeof Account> {
return `${formattedValue} in ${currentYear}`;
}

removeFileExtension(cardUrl: string) {
return cardUrl.replace(/\.[^/.]+$/, '');
}

<template>
<AccountPageLayout>
<:header>
Expand Down Expand Up @@ -485,7 +489,23 @@ export class IsolatedTemplate extends Component<typeof Account> {
{{#if this.hasActiveTasks}}
{{#each this.activeTasks.instances as |task|}}
{{#let (getComponent task) as |Component|}}
<Component @format='embedded' @displayContainer={{false}} />
<div
{{@context.cardComponentModifier
cardId=task.id
format='data'
fieldType=undefined
fieldName=undefined
}}
data-test-cards-grid-item={{this.removeFileExtension
task.id
}}
data-cards-grid-item={{this.removeFileExtension task.id}}
>
<Component
@format='embedded'
@displayContainer={{false}}
/>
</div>
{{/let}}
{{/each}}
{{else}}
Expand Down
27 changes: 23 additions & 4 deletions packages/experiments-realm/crm/deal.gts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ export class IsolatedTemplate extends Component<typeof Deal> {
return null;
}

removeFileExtension(cardUrl: string) {
return cardUrl.replace(/\.[^/.]+$/, '');
}

<template>
<DealPageLayout>
<:header>
Expand Down Expand Up @@ -461,10 +465,25 @@ export class IsolatedTemplate extends Component<typeof Deal> {
{{#if this.hasActiveTasks}}
{{#each this.activeTasks.instances as |task|}}
{{#let (getComponent task) as |Component|}}
<Component
@format='embedded'
@displayContainer={{false}}
/>
<div
{{@context.cardComponentModifier
cardId=task.id
format='data'
fieldType=undefined
fieldName=undefined
}}
data-test-cards-grid-item={{this.removeFileExtension
task.id
}}
data-cards-grid-item={{this.removeFileExtension
task.id
}}
>
<Component
@format='embedded'
@displayContainer={{false}}
/>
</div>
{{/let}}
{{/each}}
{{else}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,56 @@ import { TrackedSet } from 'tracked-built-ins';
import { AddButton, Tooltip, Pill } from '@cardstack/boxel-ui/components';
import { and, cn, gt, not } from '@cardstack/boxel-ui/helpers';

import { chooseCard, baseCardRef } from '@cardstack/runtime-common';
import {
chooseCard,
baseCardRef,
isCardInstance,
} from '@cardstack/runtime-common';

import CardPill from '@cardstack/host/components/card-pill';

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

interface Signature {
Element: HTMLDivElement;
Args: {
autoAttachedCards?: TrackedSet<CardDef>;
autoAttachedFiles?: FileDef[];
cardsToAttach: CardDef[] | undefined;
filesToAttach: FileDef[] | undefined;
chooseCard: (card: CardDef) => void;
removeCard: (card: CardDef) => void;
maxNumberOfCards?: number;
maxNumberOfItemsToAttach?: number;
};
}

const MAX_CARDS_TO_DISPLAY = 4;
export default class AiAssistantCardPicker extends Component<Signature> {
const MAX_ITEMS_TO_DISPLAY = 4;

export default class AiAssistantAttachmentPicker extends Component<Signature> {
<template>
<div class='card-picker'>
{{#each this.cardsToDisplay as |card i|}}
{{#if (this.isCardDisplayed card i)}}
{{#if (this.isAutoAttachedCard card)}}
<div class='item-picker'>
{{#each this.itemsToDisplay as |item|}}
{{#if (this.isCard item)}}
{{#if (this.isAutoAttachedCard item)}}
<Tooltip @placement='top'>
<:trigger>
<CardPill
@card={{card}}
@card={{item}}
@isAutoAttachedCard={{true}}
@removeCard={{@removeCard}}
/>
</:trigger>

<:content>
{{#if (this.isAutoAttachedCard card)}}
{{#if (this.isAutoAttachedCard item)}}
Topmost card is shared automatically
{{/if}}
</:content>
</Tooltip>
{{else}}
<CardPill
@card={{card}}
@card={{item}}
@isAutoAttachedCard={{false}}
@removeCard={{@removeCard}}
/>
Expand All @@ -60,36 +68,36 @@ export default class AiAssistantCardPicker extends Component<Signature> {
{{/each}}
{{#if
(and
(gt this.cardsToDisplay.length MAX_CARDS_TO_DISPLAY)
(not this.isViewAllAttachedCards)
(gt this.items.length MAX_ITEMS_TO_DISPLAY)
(not this.areAllItemsDisplayed)
)
}}
<Pill
@kind='button'
{{on 'click' this.toggleViewAllAttachedCards}}
data-test-view-all
>
View All ({{this.cardsToDisplay.length}})
View All ({{this.items.length}})
</Pill>
{{/if}}
{{#if this.canDisplayAddButton}}
<AddButton
class={{cn 'attach-button' icon-only=this.cardsToDisplay.length}}
class={{cn 'attach-button' icon-only=this.itemsToDisplay.length}}
@variant='pill'
@iconWidth='14'
@iconHeight='14'
{{on 'click' this.chooseCard}}
@disabled={{this.doChooseCard.isRunning}}
data-test-choose-card-btn
>
<span class={{if this.cardsToDisplay.length 'boxel-sr-only'}}>
<span class={{if this.itemsToDisplay.length 'boxel-sr-only'}}>
Add Card
</span>
</AddButton>
{{/if}}
</div>
<style scoped>
.card-picker {
.item-picker {
background-color: var(--boxel-light);
color: var(--boxel-dark);
display: flex;
Expand Down Expand Up @@ -121,40 +129,50 @@ export default class AiAssistantCardPicker extends Component<Signature> {
</style>
</template>

@tracked isViewAllAttachedCards = false;
@tracked areAllItemsDisplayed = false;

@action
private toggleViewAllAttachedCards() {
this.isViewAllAttachedCards = !this.isViewAllAttachedCards;
this.areAllItemsDisplayed = !this.areAllItemsDisplayed;
}

@action
private isCardDisplayed(card: CardDef, index: number): boolean {
if (
this.isViewAllAttachedCards ||
this.cardsToDisplay.length <= MAX_CARDS_TO_DISPLAY
) {
return !!card.id;
} else {
// If attached cards more than four,
// displays the first three cards.
return !!card.id && index < MAX_CARDS_TO_DISPLAY - 1;
isCard = (item: CardDef | FileDef): item is CardDef => {
return isCardInstance(item);
};

isAutoAttachedCard = (card: CardDef) => {
if (this.args.autoAttachedCards === undefined) {
return false;
}
}
return this.args.autoAttachedCards.has(card);
};

private get cardsToDisplay() {
private get items() {
let cards = this.args.cardsToAttach ?? [];
let files = this.args.filesToAttach ?? [];
if (this.args.autoAttachedCards) {
cards = [...new Set([...this.args.autoAttachedCards, ...cards])];
}
return cards;

cards = cards.filter((card) => card.id); // Dont show new unsaved cards

if (this.args.autoAttachedFiles) {
files = [...new Set([...this.args.autoAttachedFiles, ...files])];
}
return [...cards, ...files];
}

private get itemsToDisplay() {
return this.areAllItemsDisplayed
? this.items
: this.items.slice(0, MAX_ITEMS_TO_DISPLAY);
}

private get canDisplayAddButton() {
if (!this.args.maxNumberOfCards || !this.args.cardsToAttach) {
if (!this.args.maxNumberOfItemsToAttach || !this.args.cardsToAttach) {
return true;
}
return this.args.cardsToAttach.length < this.args.maxNumberOfCards;
return this.args.cardsToAttach.length < this.args.maxNumberOfItemsToAttach;
}

@action
Expand All @@ -171,12 +189,4 @@ export default class AiAssistantCardPicker extends Component<Signature> {
});
return chosenCard;
});

@action
private isAutoAttachedCard(card: CardDef) {
if (this.args.autoAttachedCards === undefined) {
return false;
}
return this.args.autoAttachedCards.has(card);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ import CardCatalogModal from '@cardstack/host/components/card-catalog/modal';

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

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

import AiAssistantAttachmentPicker from './index';

export default class AiAssistantCardPickerUsage extends Component {
cards: TrackedArray<CardDef> = new TrackedArray([]);
@tracked maxNumberOfCards: number | undefined = undefined;
@tracked autoAttachedCards?: TrackedSet<CardDef> = new TrackedSet();
@tracked autoAttachedFiles: TrackedArray<FileDef> = new TrackedArray([]);
@tracked filesToAttach: TrackedArray<FileDef> = new TrackedArray([]);

@action chooseCard(card: CardDef) {
if (!this.cards?.find((c) => c.id === card.id)) {
Expand All @@ -31,19 +35,21 @@ export default class AiAssistantCardPickerUsage extends Component {
}

<template>
<FreestyleUsage @name='AiAssistant::CardPicker'>
<FreestyleUsage @name='AiAssistant::AttachmentPicker'>
<:description>
Card picker for AI Assistant chat input. It allows to pick a card from
the card catalog. Selected card is attached to the message in atom
format.
the card catalog, or a file. Selected card is attached to the message in
atom format.
</:description>
<:example>
<AiAssistantCardPicker
<AiAssistantAttachmentPicker
@autoAttachedCards={{this.autoAttachedCards}}
@cardsToAttach={{this.cards}}
@chooseCard={{this.chooseCard}}
@removeCard={{this.removeCard}}
@maxNumberOfCards={{this.maxNumberOfCards}}
@maxNumberOfItemsToAttach={{this.maxNumberOfCards}}
@autoAttachedFiles={{this.autoAttachedFiles}}
@filesToAttach={{this.filesToAttach}}
/>
<CardCatalogModal />
</:example>
Expand All @@ -58,6 +64,16 @@ export default class AiAssistantCardPickerUsage extends Component {
@description='A card automatically attached to the message from the top of the stack.'
@value={{this.autoAttachedCards}}
/>
<Args.Object
@name='autoAttachedFiles'
@description='An array of files automatically attached to the message.'
@value={{this.autoAttachedFiles}}
/>
<Args.Object
@name='filesToAttach'
@description='An array of files to attach to the message.'
@value={{this.filesToAttach}}
/>
<Args.Action
@name='chooseCard'
@description='Action to be taken when a card is chosen'
Expand Down
Loading

0 comments on commit 4e07232

Please sign in to comment.