From a77fef44ba58833f7ab35d047974c970cfd150f0 Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 11:17:04 +0800 Subject: [PATCH 01/99] add phone input component --- packages/boxel-ui/addon/package.json | 2 + packages/boxel-ui/addon/src/components.ts | 2 + .../input-group/accessories/index.gts | 1 + .../src/components/phone-input/index.gts | 152 ++++++++++++++++++ .../src/components/phone-input/usage.gts | 28 ++++ packages/boxel-ui/addon/src/usage.ts | 2 + pnpm-lock.yaml | 14 ++ 7 files changed, 201 insertions(+) create mode 100644 packages/boxel-ui/addon/src/components/phone-input/index.gts create mode 100644 packages/boxel-ui/addon/src/components/phone-input/usage.gts diff --git a/packages/boxel-ui/addon/package.json b/packages/boxel-ui/addon/package.json index b18526b78a..3741245517 100644 --- a/packages/boxel-ui/addon/package.json +++ b/packages/boxel-ui/addon/package.json @@ -41,6 +41,7 @@ "@floating-ui/dom": "^1.6.3", "@glint/template": "1.3.0", "classnames": "^2.3.2", + "countries-list": "^3.1.1", "dayjs": "^1.11.7", "ember-basic-dropdown": "^8.0.0", "ember-css-url": "^1.0.0", @@ -59,6 +60,7 @@ "ember-velcro": "^2.1.3", "file-loader": "^6.2.0", "focus-trap": "^7.4.3", + "libphonenumber-js": "^1.11.15", "lodash": "^4.17.21", "pluralize": "^8.0.0", "tracked-built-ins": "^3.2.0", diff --git a/packages/boxel-ui/addon/src/components.ts b/packages/boxel-ui/addon/src/components.ts index e880b23be7..8ba8241408 100644 --- a/packages/boxel-ui/addon/src/components.ts +++ b/packages/boxel-ui/addon/src/components.ts @@ -41,6 +41,7 @@ import BoxelMultiSelect, { import Pill from './components/pill/index.gts'; import ProgressBar from './components/progress-bar/index.gts'; import ProgressRadial from './components/progress-radial/index.gts'; +import PhoneInput from './components/phone-input/index.gts'; import RadioInput from './components/radio-input/index.gts'; import RealmIcon from './components/realm-icon/index.gts'; import ResizablePanelGroup, { @@ -95,6 +96,7 @@ export { Pill, ProgressBar, ProgressRadial, + PhoneInput, RadioInput, RealmIcon, ResizablePanel, diff --git a/packages/boxel-ui/addon/src/components/input-group/accessories/index.gts b/packages/boxel-ui/addon/src/components/input-group/accessories/index.gts index 60e3699ae4..6502c4938e 100644 --- a/packages/boxel-ui/addon/src/components/input-group/accessories/index.gts +++ b/packages/boxel-ui/addon/src/components/input-group/accessories/index.gts @@ -152,6 +152,7 @@ export const Select: TemplateOnlyComponent = @onChange={{@onChange}} @onBlur={{@onBlur}} @matchTriggerWidth={{@matchTriggerWidth}} + @selectedItemComponent={{@selectedItemComponent}} data-test-boxel-input-group-select-accessory-trigger ...attributes as |item| diff --git a/packages/boxel-ui/addon/src/components/phone-input/index.gts b/packages/boxel-ui/addon/src/components/phone-input/index.gts new file mode 100644 index 0000000000..78fb705d6e --- /dev/null +++ b/packages/boxel-ui/addon/src/components/phone-input/index.gts @@ -0,0 +1,152 @@ +import type { TemplateOnlyComponent } from '@ember/component/template-only'; +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { type TCountryCode, countries, getEmojiFlag } from 'countries-list'; +import { + type CountryCallingCode, + type CountryCode, + getCountries, + getCountryCallingCode, + getExampleNumber, + isValidPhoneNumber, +} from 'libphonenumber-js'; +// @ts-expect-error import not found +import examples from 'libphonenumber-js/mobile/examples'; +import { debounce } from 'lodash'; + +import { type InputValidationState } from '../input/index.gts'; +import BoxelInputGroup from '../input-group/index.gts'; + +interface Signature { + Args: { + value: string; + onInput: (value: string) => void; + }; + Blocks: { + default: []; + }; + Element: HTMLElement; +} + +interface CountryInfo { + callingCode?: CountryCallingCode; + code: CountryCode; + name?: string; + flag?: string; + example?: { + callingCode: CountryCallingCode; + nationalNumber: string; + }; +} + +const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { + let example = getExampleNumber(countryCode, examples); + let callingCode = getCountryCallingCode(countryCode); + let c = countries[countryCode as TCountryCode]; + if (c === undefined) { + return undefined; + } + return { + code: countryCode, + callingCode, + name: c ? c.name : undefined, + flag: getEmojiFlag(countryCode as TCountryCode), + example: example + ? { + callingCode, + nationalNumber: example.format('NATIONAL'), + } + : undefined, + }; +}; + +export default class PhoneInput extends Component { + @tracked items: Array = []; + @tracked selectedItem: CountryInfo = getCountryInfo('US')!; + @tracked validationState: InputValidationState = 'initial'; + @tracked input: string = this.args.value ?? ''; + + @action onSelectItem(item: CountryInfo): void { + this.selectedItem = item; + if (this.input.length > 0) { + this.validationState = isValidPhoneNumber( + this.input, + this.selectedItem.code, + ) + ? 'valid' + : 'invalid'; + } + } + + constructor(owner: unknown, args: any) { + super(owner, args); + this.items = getCountries() + .map((code) => { + return getCountryInfo(code); + }) + .filter((c) => c !== undefined) as CountryInfo[]; + } + + get placeholder(): string | undefined { + if (this.selectedItem) { + return this.selectedItem.example?.nationalNumber; + } + return undefined; + } + + get phoneNumber(): string { + return `+${this.selectedItem.callingCode} `; + } + + @action onInput(v: string): void { + this.debouncedInput(v); + } + + private debouncedInput = debounce((input: string) => { + this.validationState = isValidPhoneNumber(input, this.selectedItem.code) + ? 'valid' + : 'invalid'; + this.input = input; + //save when the state is valid + if (this.validationState === 'valid') { + this.args.onInput(this.input); + } + }, 300); + + +} + +export interface SelectedItemSignature { + Args: { + option: any; + }; + Element: HTMLElement; +} + +const PhoneSelectedItem: TemplateOnlyComponent = + ; diff --git a/packages/boxel-ui/addon/src/components/phone-input/usage.gts b/packages/boxel-ui/addon/src/components/phone-input/usage.gts new file mode 100644 index 0000000000..2513a002b5 --- /dev/null +++ b/packages/boxel-ui/addon/src/components/phone-input/usage.gts @@ -0,0 +1,28 @@ +import Component from '@glimmer/component'; +import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import PhoneInput from './index.gts'; + +export default class PhoneInputUsage extends Component { + @tracked value = ''; + + @action onInput(value: string): void { + this.value = value; + } + + +} diff --git a/packages/boxel-ui/addon/src/usage.ts b/packages/boxel-ui/addon/src/usage.ts index d639b7f398..ccec881476 100644 --- a/packages/boxel-ui/addon/src/usage.ts +++ b/packages/boxel-ui/addon/src/usage.ts @@ -10,6 +10,7 @@ import CardContainerUsage from './components/card-container/usage.gts'; import CardContentContainerUsage from './components/card-content-container/usage.gts'; import CardHeaderUsage from './components/card-header/usage.gts'; import CircleSpinnerUsage from './components/circle-spinner/usage.gts'; +import PhoneInputUsage from './components/phone-input/usage.gts'; import DateRangePickerUsage from './components/date-range-picker/usage.gts'; import DragAndDropUsage from './components/drag-and-drop/usage.gts'; import DropdownTriggerUsage from './components/dropdown/trigger/usage.gts'; @@ -48,6 +49,7 @@ export const ALL_USAGE_COMPONENTS = [ ['CardContentContainer', CardContentContainerUsage], ['CardHeader', CardHeaderUsage], ['CircleSpinner', CircleSpinnerUsage], + ['PhoneInput', PhoneInputUsage], ['DateRangePicker', DateRangePickerUsage], ['DragAndDrop', DragAndDropUsage], ['DropdownTrigger', DropdownTriggerUsage], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7c1bb03c..de2d221710 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -773,6 +773,9 @@ importers: classnames: specifier: ^2.3.2 version: 2.3.2 + countries-list: + specifier: ^3.1.1 + version: 3.1.1 dayjs: specifier: ^1.11.7 version: 1.11.7 @@ -830,6 +833,9 @@ importers: focus-trap: specifier: ^7.4.3 version: 7.4.3 + libphonenumber-js: + specifier: ^1.11.15 + version: 1.11.15 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -11933,6 +11939,10 @@ packages: typescript: 5.1.6 dev: true + /countries-list@3.1.1: + resolution: {integrity: sha512-nPklKJ5qtmY5MdBKw1NiBAoyx5Sa7p2yPpljZyQ7gyCN1m+eMFs9I6CT37Mxt8zvR5L3VzD3DJBE4WQzX3WF4A==} + dev: false + /crc32-stream@2.0.0: resolution: {integrity: sha512-UjZSqFCbn+jZUHJIh6Y3vMF7EJLcJWNm4tKDf2peJRwlZKHvkkvOMTvAei6zjU9gO1xONVr3rRFw0gixm2eUng==} engines: {node: '>= 0.10.0'} @@ -19218,6 +19228,10 @@ packages: type-check: 0.4.0 dev: true + /libphonenumber-js@1.11.15: + resolution: {integrity: sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==} + dev: false + /line-column@1.0.2: resolution: {integrity: sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==} dependencies: From c099ff38dc03d6be400985a5e83667b2b1220697 Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 11:47:14 +0800 Subject: [PATCH 02/99] fix lint --- packages/boxel-ui/addon/src/components.ts | 4 ++-- .../boxel-ui/addon/src/components/phone-input/index.gts | 8 ++++---- .../boxel-ui/addon/src/components/phone-input/usage.gts | 5 +++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/boxel-ui/addon/src/components.ts b/packages/boxel-ui/addon/src/components.ts index 8ba8241408..6bd10aae3e 100644 --- a/packages/boxel-ui/addon/src/components.ts +++ b/packages/boxel-ui/addon/src/components.ts @@ -38,10 +38,10 @@ import Modal from './components/modal/index.gts'; import BoxelMultiSelect, { BoxelMultiSelectBasic, } from './components/multi-select/index.gts'; +import PhoneInput from './components/phone-input/index.gts'; import Pill from './components/pill/index.gts'; import ProgressBar from './components/progress-bar/index.gts'; import ProgressRadial from './components/progress-radial/index.gts'; -import PhoneInput from './components/phone-input/index.gts'; import RadioInput from './components/radio-input/index.gts'; import RealmIcon from './components/realm-icon/index.gts'; import ResizablePanelGroup, { @@ -93,10 +93,10 @@ export { Menu, Message, Modal, + PhoneInput, Pill, ProgressBar, ProgressRadial, - PhoneInput, RadioInput, RealmIcon, ResizablePanel, diff --git a/packages/boxel-ui/addon/src/components/phone-input/index.gts b/packages/boxel-ui/addon/src/components/phone-input/index.gts index 78fb705d6e..27610cd90c 100644 --- a/packages/boxel-ui/addon/src/components/phone-input/index.gts +++ b/packages/boxel-ui/addon/src/components/phone-input/index.gts @@ -20,8 +20,8 @@ import BoxelInputGroup from '../input-group/index.gts'; interface Signature { Args: { - value: string; onInput: (value: string) => void; + value: string; }; Blocks: { default: []; @@ -32,12 +32,12 @@ interface Signature { interface CountryInfo { callingCode?: CountryCallingCode; code: CountryCode; - name?: string; - flag?: string; example?: { callingCode: CountryCallingCode; nationalNumber: string; }; + flag?: string; + name?: string; } const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { @@ -148,5 +148,5 @@ export interface SelectedItemSignature { Element: HTMLElement; } -const PhoneSelectedItem: TemplateOnlyComponent = +const PhoneSelectedItem: TemplateOnlyComponent = [ ; diff --git a/packages/boxel-ui/addon/src/components/phone-input/usage.gts b/packages/boxel-ui/addon/src/components/phone-input/usage.gts index 2513a002b5..64b80d0701 100644 --- a/packages/boxel-ui/addon/src/components/phone-input/usage.gts +++ b/packages/boxel-ui/addon/src/components/phone-input/usage.gts @@ -1,7 +1,8 @@ +import { action } from '@ember/object'; import Component from '@glimmer/component'; -import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; import { tracked } from '@glimmer/tracking'; -import { action } from '@ember/object'; +import FreestyleUsage from 'ember-freestyle/components/freestyle/usage'; + import PhoneInput from './index.gts'; export default class PhoneInputUsage extends Component { From 46f8347dccb70c325a0293fcae83f3e3aaad645c Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 12:03:32 +0800 Subject: [PATCH 03/99] some small fix --- .../addon/src/components/phone-input/index.gts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/boxel-ui/addon/src/components/phone-input/index.gts b/packages/boxel-ui/addon/src/components/phone-input/index.gts index 27610cd90c..d4a8d9ac4b 100644 --- a/packages/boxel-ui/addon/src/components/phone-input/index.gts +++ b/packages/boxel-ui/addon/src/components/phone-input/index.gts @@ -45,7 +45,10 @@ const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { let callingCode = getCountryCallingCode(countryCode); let c = countries[countryCode as TCountryCode]; if (c === undefined) { - return undefined; + //here some country code may not be found due to the discrepancy between countries-list and libphonenumber-js library + //Only scenario where this is true is the usage of "AC" + //Most countries consider "AC" Ascension Island as part of "SH" Saint Helena + return; } return { code: countryCode, @@ -61,7 +64,7 @@ const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { }; }; -export default class PhoneInput extends Component { +class PhoneInput extends Component { @tracked items: Array = []; @tracked selectedItem: CountryInfo = getCountryInfo('US')!; @tracked validationState: InputValidationState = 'initial'; @@ -148,5 +151,7 @@ export interface SelectedItemSignature { Element: HTMLElement; } -const PhoneSelectedItem: TemplateOnlyComponent = [ +const PhoneSelectedItem: TemplateOnlyComponent = ; + +export default PhoneInput; From 0b43793b7da7e779ff403aab385327aa4c63e9b2 Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 13:33:21 +0800 Subject: [PATCH 04/99] fix lint --- .../addon/src/components/phone-input/index.gts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/boxel-ui/addon/src/components/phone-input/index.gts b/packages/boxel-ui/addon/src/components/phone-input/index.gts index d4a8d9ac4b..dd96bdd7c9 100644 --- a/packages/boxel-ui/addon/src/components/phone-input/index.gts +++ b/packages/boxel-ui/addon/src/components/phone-input/index.gts @@ -148,10 +148,16 @@ export interface SelectedItemSignature { Args: { option: any; }; - Element: HTMLElement; + Element: HTMLDivElement; } const PhoneSelectedItem: TemplateOnlyComponent = - ; +// eslint-disable-next-line prettier/prettier TODO: fix this eslint error --> Insert `·[` prettier/prettier + ; export default PhoneInput; From 1e4e1519ddc9294b31368c1f7ddb1ccc006ebfd0 Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 11:29:52 +0800 Subject: [PATCH 05/99] add phone number field --- .../83289456-e32b-462c-8101-010aaa62527c.json | 23 ++++ packages/experiments-realm/phone-number.gts | 101 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 packages/experiments-realm/CardWithContactPhoneNumber/83289456-e32b-462c-8101-010aaa62527c.json create mode 100644 packages/experiments-realm/phone-number.gts diff --git a/packages/experiments-realm/CardWithContactPhoneNumber/83289456-e32b-462c-8101-010aaa62527c.json b/packages/experiments-realm/CardWithContactPhoneNumber/83289456-e32b-462c-8101-010aaa62527c.json new file mode 100644 index 0000000000..b772f91c28 --- /dev/null +++ b/packages/experiments-realm/CardWithContactPhoneNumber/83289456-e32b-462c-8101-010aaa62527c.json @@ -0,0 +1,23 @@ +{ + "data": { + "type": "card", + "attributes": { + "contactPhone": { + "value": "01355889283", + "type": { + "index": 2, + "label": "Work" + } + }, + "title": null, + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../phone-number", + "name": "CardWithContactPhoneNumber" + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/phone-number.gts b/packages/experiments-realm/phone-number.gts new file mode 100644 index 0000000000..de893c1cc9 --- /dev/null +++ b/packages/experiments-realm/phone-number.gts @@ -0,0 +1,101 @@ +import { + contains, + field, + Component, + FieldDef, + StringField, + CardDef, +} from 'https://cardstack.com/base/card-api'; +import { LooseGooseyField, LooseyGooseyData } from './loosey-goosey'; +import { PhoneInput } from '@cardstack/boxel-ui/components'; +import Phone from '@cardstack/boxel-icons/phone'; +import { RadioInput } from '@cardstack/boxel-ui/components'; +import { tracked } from '@glimmer/tracking'; +import { fn } from '@ember/helper'; +import { action } from '@ember/object'; + +class PhoneNumberTypeEdit extends Component { + @tracked label: string | undefined = this.args.model.label; + + get types() { + return PhoneNumberType.values; + } + + get selected() { + return this.types?.find((type) => { + return type.label === this.label; + }); + } + + @action onSelect(type: LooseyGooseyData): void { + this.label = type.label; + this.args.model.label = this.selected?.label; + this.args.model.index = this.selected?.index; + } + +} + +export class PhoneNumberType extends LooseGooseyField { + static displayName = 'Phone Number Type'; + static values = [ + { index: 0, label: 'Mobile' }, + { index: 1, label: 'Home' }, + { index: 2, label: 'Work' }, + ]; + static edit = PhoneNumberTypeEdit; +} + +export class PhoneNumber extends StringField { + static displayName = 'Phone Number'; + + static edit = class Edit extends Component { + + }; + + static atom = class Atom extends Component { + + }; +} + +export class ContactPhoneNumber extends FieldDef { + @field value = contains(PhoneNumber); + @field type = contains(PhoneNumberType); + + static atom = class Atom extends Component { + + }; +} + +//TODO: Remove this after implementing the phone number +export class CardWithContactPhoneNumber extends CardDef { + @field contactPhone = contains(ContactPhoneNumber); + + static isolated = class Isolated extends Component { + + }; +} From e430bdc011d0ab35999e8ce2a8db5ca7b3ef374d Mon Sep 17 00:00:00 2001 From: tintinthong Date: Mon, 2 Dec 2024 11:45:47 +0800 Subject: [PATCH 06/99] make atom field for contact phone number --- packages/experiments-realm/phone-number.gts | 45 ++++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/experiments-realm/phone-number.gts b/packages/experiments-realm/phone-number.gts index de893c1cc9..ec9db4858b 100644 --- a/packages/experiments-realm/phone-number.gts +++ b/packages/experiments-realm/phone-number.gts @@ -7,12 +7,12 @@ import { CardDef, } from 'https://cardstack.com/base/card-api'; import { LooseGooseyField, LooseyGooseyData } from './loosey-goosey'; -import { PhoneInput } from '@cardstack/boxel-ui/components'; -import Phone from '@cardstack/boxel-icons/phone'; +import { PhoneInput, Pill } from '@cardstack/boxel-ui/components'; import { RadioInput } from '@cardstack/boxel-ui/components'; import { tracked } from '@glimmer/tracking'; import { fn } from '@ember/helper'; import { action } from '@ember/object'; +import PhoneIcon from '@cardstack/boxel-icons/phone'; class PhoneNumberTypeEdit extends Component { @tracked label: string | undefined = this.args.model.label; @@ -70,10 +70,23 @@ export class PhoneNumber extends StringField { static atom = class Atom extends Component { }; } @@ -84,7 +97,27 @@ export class ContactPhoneNumber extends FieldDef { static atom = class Atom extends Component { }; } From 2dbb50955cabe3347a6f8869bd38dacdc7709e25 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Mon, 16 Dec 2024 22:11:05 +0000 Subject: [PATCH 07/99] Extract tool choice setting from events --- packages/ai-bot/helpers.ts | 51 ++++++---- .../ai-bot/tests/prompt-construction-test.ts | 97 ------------------- 2 files changed, 34 insertions(+), 114 deletions(-) diff --git a/packages/ai-bot/helpers.ts b/packages/ai-bot/helpers.ts index 1eaef7a605..f6b241cc4a 100644 --- a/packages/ai-bot/helpers.ts +++ b/packages/ai-bot/helpers.ts @@ -3,9 +3,10 @@ import { type LooseSingleCardDocument, type CardResource, } from '@cardstack/runtime-common'; -import { getSearchTool } from '@cardstack/runtime-common/helpers/ai'; +import { ToolChoice } from '@cardstack/runtime-common/helpers/ai'; import type { MatrixEvent as DiscreteMatrixEvent, + CardMessageEvent, CardFragmentContent, CommandEvent, CommandResultEvent, @@ -51,7 +52,7 @@ export interface PromptParts { messages: OpenAIPromptMessage[]; model: string; history: DiscreteMatrixEvent[]; - toolChoice: 'auto' | 'none'; + toolChoice: ToolChoice; } export type Message = CommandMessage | TextMessage; @@ -75,13 +76,14 @@ export function getPromptParts( ); let skills = getEnabledSkills(eventList, cardFragments); let tools = getTools(history, aiBotUserId); + let toolChoice = getToolChoice(history, aiBotUserId); let messages = getModifyPrompt(history, aiBotUserId, tools, skills); return { tools, messages, model: 'openai/gpt-4o', history, - toolChoice: 'auto', + toolChoice: toolChoice, }; } @@ -293,31 +295,46 @@ export function getRelevantCards( }; } -export function getTools( +function getLastUserMessage( history: DiscreteMatrixEvent[], aiBotUserId: string, -): Tool[] { - // TODO: there should be no default tools defined in the ai-bot, tools must be determined by the host - let searchTool = getSearchTool(); - let tools = [searchTool as Tool]; - // Just get the users messages +): CardMessageEvent | undefined { const userMessages = history.filter((event) => event.sender !== aiBotUserId); // Get the last message if (userMessages.length === 0) { - // If the user has sent no messages, return tools that are available by default - return tools; + // If the user has sent no messages, return undefined + return undefined; } const lastMessage = userMessages[userMessages.length - 1]; if ( lastMessage.type === 'm.room.message' && - lastMessage.content.msgtype === 'org.boxel.message' && - lastMessage.content.data?.context?.tools?.length + lastMessage.content.msgtype === 'org.boxel.message' ) { + return lastMessage as CardMessageEvent; + } + return undefined; +} + +export function getTools( + history: DiscreteMatrixEvent[], + aiBotUserId: string, +): Tool[] { + const lastMessage = getLastUserMessage(history, aiBotUserId); + if (lastMessage && lastMessage.content.data?.context?.tools?.length) { return lastMessage.content.data.context.tools; - } else { - // If it's a different message type, or there are no tools, return tools that are available by default - return tools; } + return []; +} + +export function getToolChoice( + history: DiscreteMatrixEvent[], + aiBotUserId: string, +): ToolChoice { + let lastMessage = getLastUserMessage(history, aiBotUserId); + if (lastMessage && lastMessage.content.data?.context?.toolChoice) { + return lastMessage.content.data.context.toolChoice; + } + return 'auto'; } export function isCommandResultEvent( @@ -489,7 +506,7 @@ export function getModifyPrompt( if (tools.length == 0) { systemMessage += - 'You are unable to edit any cards, the user has not given you access, they need to open the card on the stack and let it be auto-attached. However, you are allowed to search for cards.'; + 'You are unable to edit any cards, the user has not given you access, they need to open the card on the stack and let it be auto-attached.'; } let messages: OpenAIPromptMessage[] = [ diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index 6c834dcefd..cff8f4a234 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -719,103 +719,6 @@ module('getModifyPrompt', () => { ); }); - test('If there are no functions in the last message from the user, store only searchTool', () => { - const history: DiscreteMatrixEvent[] = [ - { - type: 'm.room.message', - sender: '@ian:localhost', - content: { - msgtype: 'org.boxel.message', - format: 'org.matrix.custom.html', - body: 'Just a regular message', - formatted_body: 'Just a regular message', - data: { - context: { - openCardIds: [], - tools: [], - submode: 'interact', - }, - }, - }, - room_id: 'room1', - origin_server_ts: 1696813813167, - unsigned: { - age: 115498, - transaction_id: '2', - }, - event_id: '$AZ65GbUls1UdpiOPD_AfSVu8RyiFYN1vltmUKmUnV4c', - status: EventStatus.SENT, - }, - ]; - const functions = getTools(history, '@aibot:localhost'); - assert.equal(functions.length, 1); - assert.deepEqual(functions[0], getSearchTool()); - }); - - test('If a user stops sharing their context then ignore function calls with exception of searchTool', () => { - const history: DiscreteMatrixEvent[] = [ - { - type: 'm.room.message', - sender: '@ian:localhost', - content: { - msgtype: 'org.boxel.message', - format: 'org.matrix.custom.html', - body: 'set the name to dave', - formatted_body: '

set the name to dave

\n', - data: { - context: { - openCardIds: ['http://localhost:4201/experiments/Friend/1'], - tools: [ - getPatchTool('http://localhost:4201/experiments/Friend/1', { - attributes: { - firstName: { type: 'string' }, - }, - }), - ], - submode: 'interact', - }, - }, - }, - room_id: 'room1', - origin_server_ts: 1696813813166, - unsigned: { - age: 115498, - transaction_id: '1', - }, - event_id: '1', - status: EventStatus.SENT, - }, - { - type: 'm.room.message', - sender: '@ian:localhost', - content: { - msgtype: 'org.boxel.message', - format: 'org.matrix.custom.html', - body: 'Just a regular message', - formatted_body: 'Just a regular message', - data: { - context: { - openCardIds: [], - tools: [], - submode: 'interact', - }, - }, - }, - room_id: 'room1', - origin_server_ts: 1696813813167, - unsigned: { - age: 115498, - transaction_id: '2', - }, - event_id: '2', - status: EventStatus.SENT, - }, - ]; - const functions = getTools(history, '@aibot:localhost'); - assert.equal(functions.length, 1); - assert.deepEqual(functions[0], getSearchTool()); - }); - test("Don't break when there is an older format type with open cards", () => { const history: DiscreteMatrixEvent[] = [ { From af8803801299bfda079d854b0d76711c3f65024a Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Mon, 16 Dec 2024 22:14:20 +0000 Subject: [PATCH 08/99] Support forcing the LLM to call a command --- packages/host/app/services/matrix-service.ts | 25 ++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 8d2f648de4..51392b3b00 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -33,6 +33,7 @@ import { basicMappings, generateJsonSchemaForCardType, getSearchTool, + type ToolChoice, } from '@cardstack/runtime-common/helpers/ai'; import { getPatchTool } from '@cardstack/runtime-common/helpers/ai'; @@ -629,15 +630,34 @@ export default class MatrixService extends Service { show?: boolean; // if truthy, ensure the side panel is open to the room prompt: string; attachedCards?: CardDef[]; - commands?: { command: Command; autoExecute: boolean }[]; + commands?: { + command: Command; + autoExecute: boolean; + force?: boolean; + }[]; }): Promise<{ roomId: string }> { let roomId = params.roomId; let html = markdownToHtml(params.prompt); let mappings = await basicMappings(this.loaderService.loader); let tools = []; - for (let { command, autoExecute } of params.commands ?? []) { + let toolChoice: ToolChoice = 'auto'; + for (let { command, autoExecute, force } of params.commands ?? []) { // get a registered name for the command let name = this.commandService.registerCommand(command, autoExecute); + // If we want to force the LLM to call a specific command, we need to set the toolChoice to the name of the command + // We can only force one command at a time. While some models support lists, OpenRouter currently only allows one. + if (force) { + // If we already have set it to something specific we should error out when a second is set + if (toolChoice !== 'auto') { + throw new Error(`Cannot force multiple commands in a single message`); + } + toolChoice = { + type: 'function', + function: { + name, + }, + }; + } tools.push({ type: 'function', function: { @@ -676,6 +696,7 @@ export default class MatrixService extends Service { attachedCardsEventIds, context: { tools, + toolChoice, }, }, } as CardMessageContent); From f9b49452be76431a400dd97f8b9f5ba47f6ddc51 Mon Sep 17 00:00:00 2001 From: Richard Tan Date: Tue, 17 Dec 2024 19:13:08 +0800 Subject: [PATCH 09/99] Use smaller phone number library on phone input --- packages/boxel-ui/addon/package.json | 2 +- .../src/components/phone-input/index.gts | 67 ++++++++++--------- pnpm-lock.yaml | 23 +++---- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/packages/boxel-ui/addon/package.json b/packages/boxel-ui/addon/package.json index 3741245517..fcdd8ebc1a 100644 --- a/packages/boxel-ui/addon/package.json +++ b/packages/boxel-ui/addon/package.json @@ -40,6 +40,7 @@ "@embroider/addon-shim": "^1.8.9", "@floating-ui/dom": "^1.6.3", "@glint/template": "1.3.0", + "awesome-phonenumber": "^7.2.0", "classnames": "^2.3.2", "countries-list": "^3.1.1", "dayjs": "^1.11.7", @@ -60,7 +61,6 @@ "ember-velcro": "^2.1.3", "file-loader": "^6.2.0", "focus-trap": "^7.4.3", - "libphonenumber-js": "^1.11.15", "lodash": "^4.17.21", "pluralize": "^8.0.0", "tracked-built-ins": "^3.2.0", diff --git a/packages/boxel-ui/addon/src/components/phone-input/index.gts b/packages/boxel-ui/addon/src/components/phone-input/index.gts index dd96bdd7c9..fbe093a588 100644 --- a/packages/boxel-ui/addon/src/components/phone-input/index.gts +++ b/packages/boxel-ui/addon/src/components/phone-input/index.gts @@ -1,18 +1,13 @@ -import type { TemplateOnlyComponent } from '@ember/component/template-only'; import { action } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; -import { type TCountryCode, countries, getEmojiFlag } from 'countries-list'; import { - type CountryCallingCode, - type CountryCode, - getCountries, - getCountryCallingCode, - getExampleNumber, - isValidPhoneNumber, -} from 'libphonenumber-js'; -// @ts-expect-error import not found -import examples from 'libphonenumber-js/mobile/examples'; + getCountryCodeForRegionCode, + getExample, + getSupportedRegionCodes, + parsePhoneNumber, +} from 'awesome-phonenumber'; +import { type TCountryCode, countries, getEmojiFlag } from 'countries-list'; import { debounce } from 'lodash'; import { type InputValidationState } from '../input/index.gts'; @@ -30,19 +25,20 @@ interface Signature { } interface CountryInfo { - callingCode?: CountryCallingCode; - code: CountryCode; + callingCode?: string; + code: string; example?: { - callingCode: CountryCallingCode; + callingCode: string; nationalNumber: string; }; flag?: string; name?: string; } -const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { - let example = getExampleNumber(countryCode, examples); - let callingCode = getCountryCallingCode(countryCode); +const getCountryInfo = (countryCode: string): CountryInfo | undefined => { + let example = getExample(countryCode); + let callingCode = getCountryCodeForRegionCode(countryCode); + let c = countries[countryCode as TCountryCode]; if (c === undefined) { //here some country code may not be found due to the discrepancy between countries-list and libphonenumber-js library @@ -52,13 +48,13 @@ const getCountryInfo = (countryCode: CountryCode): CountryInfo | undefined => { } return { code: countryCode, - callingCode, + callingCode: callingCode.toString(), name: c ? c.name : undefined, flag: getEmojiFlag(countryCode as TCountryCode), example: example ? { - callingCode, - nationalNumber: example.format('NATIONAL'), + callingCode: callingCode.toString(), + nationalNumber: example.number?.international ?? '', } : undefined, }; @@ -73,18 +69,16 @@ class PhoneInput extends Component { @action onSelectItem(item: CountryInfo): void { this.selectedItem = item; if (this.input.length > 0) { - this.validationState = isValidPhoneNumber( - this.input, - this.selectedItem.code, - ) - ? 'valid' - : 'invalid'; + const parsedPhoneNumber = parsePhoneNumber(this.input, { + regionCode: this.selectedItem.code, + }); + this.validationState = parsedPhoneNumber.valid ? 'valid' : 'invalid'; } } constructor(owner: unknown, args: any) { super(owner, args); - this.items = getCountries() + this.items = getSupportedRegionCodes() .map((code) => { return getCountryInfo(code); }) @@ -107,10 +101,17 @@ class PhoneInput extends Component { } private debouncedInput = debounce((input: string) => { - this.validationState = isValidPhoneNumber(input, this.selectedItem.code) - ? 'valid' - : 'invalid'; this.input = input; + + if (input === '') { + this.validationState = 'initial'; + return; + } + + const parsedPhoneNumber = parsePhoneNumber(input, { + regionCode: this.selectedItem.code, + }); + this.validationState = parsedPhoneNumber.valid ? 'valid' : 'invalid'; //save when the state is valid if (this.validationState === 'valid') { this.args.onInput(this.input); @@ -151,13 +152,13 @@ export interface SelectedItemSignature { Element: HTMLDivElement; } -const PhoneSelectedItem: TemplateOnlyComponent = -// eslint-disable-next-line prettier/prettier TODO: fix this eslint error --> Insert `·[` prettier/prettier +class PhoneSelectedItem extends Component { ; + +} export default PhoneInput; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f34e130794..0fbaf3b6e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -770,6 +770,9 @@ importers: '@glint/template': specifier: 1.3.0 version: 1.3.0 + awesome-phonenumber: + specifier: ^7.2.0 + version: 7.2.0 classnames: specifier: ^2.3.2 version: 2.3.2 @@ -833,9 +836,6 @@ importers: focus-trap: specifier: ^7.4.3 version: 7.4.3 - libphonenumber-js: - specifier: ^1.11.15 - version: 1.11.15 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -4539,7 +4539,7 @@ packages: ember-source: ^4.0.0 || ^5.0.0 dependencies: '@ember/test-waiters': 3.0.2 - '@embroider/macros': 1.16.5(@glint/template@1.3.0) + '@embroider/macros': 1.16.9(@glint/template@1.3.0) '@simple-dom/interface': 1.4.0 broccoli-debug: 0.6.5 broccoli-funnel: 3.0.8 @@ -4770,7 +4770,6 @@ packages: semver: 7.6.2 transitivePeerDependencies: - supports-color - dev: false /@embroider/shared-internals@0.50.2: resolution: {integrity: sha512-l3SKn1YdxTFBjY3ylYTLHxFY0dG2XxIsjbtZt7Mm6QyZFssWBDg3oWYwBoUpkw/ysjNJf8IcI7reXhB23WXwDw==} @@ -4834,7 +4833,6 @@ packages: typescript-memoize: 1.1.1 transitivePeerDependencies: - supports-color - dev: false /@embroider/test-setup@0.47.2: resolution: {integrity: sha512-zTiNaicOeBJWQ+uAJOEg4Z4plmvF2Ckze+tMZ0sycp2o09UV72WC8s9L3pssWOGgHMV6Kl+eCfFQelWwUOqz1Q==} @@ -8458,6 +8456,7 @@ packages: /acorn-import-assertions@1.9.0(acorn@8.10.0): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 dependencies: @@ -9046,6 +9045,11 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} + /awesome-phonenumber@7.2.0: + resolution: {integrity: sha512-CqH0TyDMHiMhqGRg/kaJDiV5KRok5AdrJKvUsED9/QRQdiCjUzp32+NqEEnY+OWEDTeeqlyLsLs7SLR/sPQU2A==} + engines: {node: '>=18'} + dev: false + /aws-sdk@2.1477.0: resolution: {integrity: sha512-DLsrKosrKRe5P1E+BcJAVpOXkma4oUOrcyBUridDmUhdf9k3jj5dnL1roFuDpTmNDDhK8a1tUgY3wmXoKQtv7A==} engines: {node: '>= 10.0.0'} @@ -9893,7 +9897,6 @@ packages: engines: {node: '>=4'} dependencies: is-windows: 1.0.2 - dev: false /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} @@ -18595,7 +18598,6 @@ packages: engines: {node: '>=4'} dependencies: better-path-resolve: 1.0.0 - dev: false /is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} @@ -19241,10 +19243,6 @@ packages: type-check: 0.4.0 dev: true - /libphonenumber-js@1.11.15: - resolution: {integrity: sha512-M7+rtYi9l5RvMmHyjyoF3BHHUpXTYdJ0PezZGHNs0GyW1lO+K7jxlXpbdIb7a56h0nqLYdjIw+E+z0ciGaJP7g==} - dev: false - /line-column@1.0.2: resolution: {integrity: sha512-Ktrjk5noGYlHsVnYWh62FLVs4hTb8A3e+vucNZMgPeAOITdshMSgv4cCZQeRDjm7+goqmo6+liZwTXo+U3sVww==} dependencies: @@ -21471,7 +21469,6 @@ packages: /pkg-entry-points@1.1.1: resolution: {integrity: sha512-BhZa7iaPmB4b3vKIACoppyUoYn8/sFs17VJJtzrzPZvEnN2nqrgg911tdL65lA2m1ml6UI3iPeYbZQ4VXpn1mA==} - dev: false /pkg-up@2.0.0: resolution: {integrity: sha512-fjAPuiws93rm7mPUu21RdBnkeZNrbfCFCwfAhPWY+rR3zG0ubpe5cEReHOw5fIbfmsxEV/g2kSxGTATY3Bpnwg==} From 048951301253e983004690233c854afbc46d1ac1 Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Tue, 17 Dec 2024 11:41:46 +0000 Subject: [PATCH 10/99] Add required toolchoice type to event and helper --- packages/base/matrix-event.gts | 2 ++ packages/runtime-common/helpers/ai.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/base/matrix-event.gts b/packages/base/matrix-event.gts index febbe23c59..b207983b6a 100644 --- a/packages/base/matrix-event.gts +++ b/packages/base/matrix-event.gts @@ -3,6 +3,7 @@ import type { EventStatus, MatrixError } from 'matrix-js-sdk'; import { FunctionToolCall, type AttributesSchema, + type ToolChoice, } from '@cardstack/runtime-common/helpers/ai'; interface BaseMatrixEvent { @@ -196,6 +197,7 @@ export interface CardMessageContent { context: { openCardIds?: string[]; tools: Tool[]; + toolChoice?: ToolChoice; submode?: string; }; }; diff --git a/packages/runtime-common/helpers/ai.ts b/packages/runtime-common/helpers/ai.ts index 45c7768da4..8e9d3990e9 100644 --- a/packages/runtime-common/helpers/ai.ts +++ b/packages/runtime-common/helpers/ai.ts @@ -475,3 +475,13 @@ export interface FunctionToolCall { arguments: { [key: string]: any }; type: 'function'; } + +export type ToolChoice = + | 'none' + | 'auto' + | { + type: 'function'; + function: { + name: string; + }; + }; From bcfe08eb321343c6c959d159d95ae7e826caab7b Mon Sep 17 00:00:00 2001 From: Burcu Noyan Date: Tue, 17 Dec 2024 18:12:27 -0500 Subject: [PATCH 11/99] Review card subclass of BlogPost card (#1936) * add review card and sample data * rating field * user rating field * review post styling * update md content * fix icon * `review` embedded template and `rating-summary` atom template * updates * change review post content * add article image * use absolute url in query --- .../boxel-ui/addon/raw-icons/star-filled.svg | 1 + .../addon/raw-icons/star-half-fill.svg | 1 + packages/boxel-ui/addon/raw-icons/star.svg | 1 + packages/boxel-ui/addon/src/icons.gts | 9 + .../boxel-ui/addon/src/icons/star-filled.gts | 25 ++ .../addon/src/icons/star-half-fill.gts | 27 ++ packages/boxel-ui/addon/src/icons/star.gts | 25 ++ .../0b9c06fd-3833-4947-a0b8-ac24b8e71ee7.json | 51 +++ .../3a655a91-98b5-4f33-a071-b62d39218b33.json | 47 +++ .../CatalogEntry/fields/rating-field.json | 19 ++ .../7f3c8694-7fa8-41d6-a449-14337c51fa29.json | 55 ++++ .../583df6bb-5739-418a-9186-978bd72816c1.json | 17 + packages/experiments-realm/blog-app.gts | 62 ++-- packages/experiments-realm/blog-post.gts | 4 +- .../experiments-realm/components/layout.gts | 36 ++- .../product-with-video-and-ratings.gts | 2 +- .../experiments-realm/ratings-summary.gts | 228 ++++++++++--- packages/experiments-realm/review-blog.gts | 37 +++ packages/experiments-realm/review.gts | 306 ++++++++++++++++++ packages/experiments-realm/video-product.gts | 2 +- 20 files changed, 871 insertions(+), 84 deletions(-) create mode 100644 packages/boxel-ui/addon/raw-icons/star-filled.svg create mode 100644 packages/boxel-ui/addon/raw-icons/star-half-fill.svg create mode 100644 packages/boxel-ui/addon/raw-icons/star.svg create mode 100644 packages/boxel-ui/addon/src/icons/star-filled.gts create mode 100644 packages/boxel-ui/addon/src/icons/star-half-fill.gts create mode 100644 packages/boxel-ui/addon/src/icons/star.gts create mode 100644 packages/experiments-realm/Author/0b9c06fd-3833-4947-a0b8-ac24b8e71ee7.json create mode 100644 packages/experiments-realm/Author/3a655a91-98b5-4f33-a071-b62d39218b33.json create mode 100644 packages/experiments-realm/CatalogEntry/fields/rating-field.json create mode 100644 packages/experiments-realm/Review/7f3c8694-7fa8-41d6-a449-14337c51fa29.json create mode 100644 packages/experiments-realm/ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1.json create mode 100644 packages/experiments-realm/review-blog.gts create mode 100644 packages/experiments-realm/review.gts diff --git a/packages/boxel-ui/addon/raw-icons/star-filled.svg b/packages/boxel-ui/addon/raw-icons/star-filled.svg new file mode 100644 index 0000000000..df0846a51c --- /dev/null +++ b/packages/boxel-ui/addon/raw-icons/star-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/boxel-ui/addon/raw-icons/star-half-fill.svg b/packages/boxel-ui/addon/raw-icons/star-half-fill.svg new file mode 100644 index 0000000000..3b504e1b80 --- /dev/null +++ b/packages/boxel-ui/addon/raw-icons/star-half-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/boxel-ui/addon/raw-icons/star.svg b/packages/boxel-ui/addon/raw-icons/star.svg new file mode 100644 index 0000000000..6373478484 --- /dev/null +++ b/packages/boxel-ui/addon/raw-icons/star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/boxel-ui/addon/src/icons.gts b/packages/boxel-ui/addon/src/icons.gts index 9dcdcafb41..42be4880ca 100644 --- a/packages/boxel-ui/addon/src/icons.gts +++ b/packages/boxel-ui/addon/src/icons.gts @@ -52,6 +52,9 @@ import Profile from './icons/profile.gts'; import Rows4 from './icons/rows-4.gts'; import Send from './icons/send.gts'; import Sparkle from './icons/sparkle.gts'; +import Star from './icons/star.gts'; +import StarFilled from './icons/star-filled.gts'; +import StarHalfFill from './icons/star-half-fill.gts'; import SuccessBordered from './icons/success-bordered.gts'; import ThreeDotsHorizontal from './icons/three-dots-horizontal.gts'; import TriangleLeft from './icons/triangle-left.gts'; @@ -109,6 +112,9 @@ export const ALL_ICON_COMPONENTS = [ Rows4, Send, Sparkle, + Star, + StarFilled, + StarHalfFill, SuccessBordered, ThreeDotsHorizontal, TriangleLeft, @@ -167,6 +173,9 @@ export { Rows4, Send, Sparkle, + Star, + StarFilled, + StarHalfFill, SuccessBordered, ThreeDotsHorizontal, TriangleLeft, diff --git a/packages/boxel-ui/addon/src/icons/star-filled.gts b/packages/boxel-ui/addon/src/icons/star-filled.gts new file mode 100644 index 0000000000..49abd75192 --- /dev/null +++ b/packages/boxel-ui/addon/src/icons/star-filled.gts @@ -0,0 +1,25 @@ +// This file is auto-generated by 'pnpm rebuild:icons' +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import type { Signature } from './types.ts'; + +const IconComponent: TemplateOnlyComponent = ; + +// @ts-expect-error this is the only way to set a name on a Template Only Component currently +IconComponent.name = 'StarFilled'; +export default IconComponent; diff --git a/packages/boxel-ui/addon/src/icons/star-half-fill.gts b/packages/boxel-ui/addon/src/icons/star-half-fill.gts new file mode 100644 index 0000000000..62ca2da0ba --- /dev/null +++ b/packages/boxel-ui/addon/src/icons/star-half-fill.gts @@ -0,0 +1,27 @@ +// This file is auto-generated by 'pnpm rebuild:icons' +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import type { Signature } from './types.ts'; + +const IconComponent: TemplateOnlyComponent = ; + +// @ts-expect-error this is the only way to set a name on a Template Only Component currently +IconComponent.name = 'StarHalfFill'; +export default IconComponent; diff --git a/packages/boxel-ui/addon/src/icons/star.gts b/packages/boxel-ui/addon/src/icons/star.gts new file mode 100644 index 0000000000..2c6be0b91c --- /dev/null +++ b/packages/boxel-ui/addon/src/icons/star.gts @@ -0,0 +1,25 @@ +// This file is auto-generated by 'pnpm rebuild:icons' +import type { TemplateOnlyComponent } from '@ember/component/template-only'; + +import type { Signature } from './types.ts'; + +const IconComponent: TemplateOnlyComponent = ; + +// @ts-expect-error this is the only way to set a name on a Template Only Component currently +IconComponent.name = 'Star'; +export default IconComponent; diff --git a/packages/experiments-realm/Author/0b9c06fd-3833-4947-a0b8-ac24b8e71ee7.json b/packages/experiments-realm/Author/0b9c06fd-3833-4947-a0b8-ac24b8e71ee7.json new file mode 100644 index 0000000000..255b579412 --- /dev/null +++ b/packages/experiments-realm/Author/0b9c06fd-3833-4947-a0b8-ac24b8e71ee7.json @@ -0,0 +1,51 @@ +{ + "data": { + "type": "card", + "attributes": { + "firstName": "Michael", + "lastName": "Anderson", + "bio": "Michael Anderson is an award-winning film critic and cultural commentator with over a decade of experience. He specializes in superhero and sci-fi genres, and is known for his insightful analysis of the Marvel Cinematic Universe. Michael hosts the popular podcast “Reel Talk” and contributes regularly to major publications. He holds a degree in Film Studies from USC and is passionate about fostering critical thinking in media consumption.", + "fullBio": "Michael Anderson isn’t just a film critic; he’s a cinematic explorer, navigating the vast universe of film with an insatiable curiosity and a keen eye for the extraordinary. With over a decade of experience, Michael has become a trusted voice in the world of cinema, particularly in the realms of superhero sagas and science fiction spectacles.\n\nBorn in the neon-lit streets of Los Angeles, Michael’s love affair with movies began in the flickering darkness of a small, family-owned theater. It was there, amidst the aroma of buttered popcorn and the whir of film reels, that he first glimpsed the power of storytelling through motion pictures. This childhood fascination evolved into a lifelong passion, eventually leading him to the hallowed halls of USC’s School of Cinematic Arts.\n\nMichael’s writing style is as dynamic as the films he critiques. He possesses a unique ability to dissect complex narratives and visual techniques, presenting them in a way that’s both intellectually stimulating and accessible to the average moviegoer. His reviews are not mere summaries, but thoughtful explorations of a film’s place in the broader cultural context.\n\nWhile Michael’s expertise spans all genres, he’s particularly renowned for his insightful analysis of the Marvel Cinematic Universe. His annual “State of the MCU” articles have become required reading for fans and industry insiders alike. Michael approaches each superhero film with the same reverence he would a Kurosawa classic, finding depth and nuance where others see mere popcorn entertainment.\n\nBeyond the written word, Michael has embraced the digital age of film criticism. His weekly podcast, “Reel Talk with Michael Anderson,” features in-depth discussions with filmmakers, actors, and fellow critics. He’s also not afraid to engage in spirited debates on social media, where his witty retorts and thoughtful arguments have earned him a devoted following.\n\nWhen he’s not in a dark theater or hunched over his laptop crafting his latest review, Michael can be found lecturing on film studies at his alma mater or mentoring the next generation of critics through his online workshop series. He believes passionately in the importance of critical thinking in media consumption and strives to foster this skill in others.\n\nAs the landscape of cinema continues to evolve, so too does Michael’s approach to criticism. He remains ever-vigilant, always ready to champion bold new voices in filmmaking or to challenge the industry when it falls short of its potential. For Michael Anderson, every frame is a world waiting to be explored, every film a journey worth taking.", + "quote": "“Cinema is not just entertainment; it’s a mirror reflecting the complexities of our world.”", + "contactLinks": [ + { + "label": "Email", + "value": "michael.anderson@companyname.com" + }, + { + "label": "LinkedIn", + "value": "https://linkedin.com/michael-anderson-boxel" + }, + { + "label": "X", + "value": "https://x.com/michael-anderson-boxel" + } + ], + "email": "michael.anderson@companyname.com", + "featuredImage": { + "imageUrl": "https://boxel-images.boxel.ai/app-assets/portraits/photo-1556474835-b0f3ac40d4d1.jpeg", + "credit": null, + "caption": null, + "altText": "Michael Anderson", + "size": "actual", + "height": null, + "width": null + }, + "description": "Senior Film Critic & Cultural Commentator", + "thumbnailURL": "https://boxel-images.boxel.ai/app-assets/portraits/photo-1556474835-b0f3ac40d4d1.jpeg" + }, + "relationships": { + "blog": { + "links": { + "self": "../ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../author", + "name": "Author" + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/Author/3a655a91-98b5-4f33-a071-b62d39218b33.json b/packages/experiments-realm/Author/3a655a91-98b5-4f33-a071-b62d39218b33.json new file mode 100644 index 0000000000..4f39fa9b48 --- /dev/null +++ b/packages/experiments-realm/Author/3a655a91-98b5-4f33-a071-b62d39218b33.json @@ -0,0 +1,47 @@ +{ + "data": { + "type": "card", + "attributes": { + "firstName": "Robert", + "lastName": "Fields", + "bio": "Robert Fields has over a decade of experience dissecting the evolving landscape of speculative cinema. Known for incisive commentary and a nuanced appreciation of narrative complexity, Fields’ insights have appeared in numerous niche publications and festival panels, shaping discussions on storytelling’s future frontiers.", + "fullBio": null, + "quote": null, + "contactLinks": [ + { + "label": "Email", + "value": "robert.fields@companyname.com" + }, + { + "label": "X", + "value": "https://www.x.com/robert-fields-boxel" + } + ], + "email": "robert.fields@personal.com", + "featuredImage": { + "imageUrl": "https://boxel-images.boxel.ai/app-assets/portraits/photo-1512485694743-9c9538b4e6e0.jpeg", + "credit": null, + "caption": null, + "altText": "Robert Fields", + "size": "actual", + "height": null, + "width": null + }, + "description": "Senior Film Critic & Cultural Commentator", + "thumbnailURL": "https://boxel-images.boxel.ai/app-assets/portraits/photo-1512485694743-9c9538b4e6e0.jpeg" + }, + "relationships": { + "blog": { + "links": { + "self": "../ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1" + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../author", + "name": "Author" + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/CatalogEntry/fields/rating-field.json b/packages/experiments-realm/CatalogEntry/fields/rating-field.json new file mode 100644 index 0000000000..2484c1a523 --- /dev/null +++ b/packages/experiments-realm/CatalogEntry/fields/rating-field.json @@ -0,0 +1,19 @@ +{ + "data": { + "type": "card", + "attributes": { + "title": "Rating Summary Field", + "isField": true, + "ref": { + "module": "../../ratings-summary", + "name": "RatingsSummary" + } + }, + "meta": { + "adoptsFrom": { + "module": "https://cardstack.com/base/catalog-entry", + "name": "CatalogEntry" + } + } + } +} diff --git a/packages/experiments-realm/Review/7f3c8694-7fa8-41d6-a449-14337c51fa29.json b/packages/experiments-realm/Review/7f3c8694-7fa8-41d6-a449-14337c51fa29.json new file mode 100644 index 0000000000..bbc777a208 --- /dev/null +++ b/packages/experiments-realm/Review/7f3c8694-7fa8-41d6-a449-14337c51fa29.json @@ -0,0 +1,55 @@ +{ + "data": { + "type": "card", + "attributes": { + "rating": { + "average": 4.5, + "count": null, + "isEditable": false + }, + "userRating": { + "average": null, + "count": null, + "isEditable": true + }, + "headline": "Singularity’s Echo – A Mind-Bending Sci-Fi Masterpiece", + "slug": null, + "body": "### A Quantum Leap in Storytelling\n\nFrom the opening shot—an endless corridor of luminescent panels that seem to fold space inward—the universe as we know it begins to warp. Commander Rellis Dratan, standing at the threshold of the research vessel *Verion-9*, is more explorer than soldier. As the ship approaches a clandestine rift of shifting hues and spiraling matter, the narrative refuses to settle into familiar patterns. Instead, it delves deeper into philosophical quandaries, blurring the line between tangible reality and a realm where perception itself is fluid. The film’s bold use of non-linear timelines, mirrored dialogues, and subtle clues hidden in the corners of each frame invites viewers to participate in the mystery, rather than passively consume it.\n\n### Stellar Performances\n\nAt the core of *Singularity’s Echo* are performances as transcendent as the cosmos it portrays. Olivia Makerran’s portrayal of Commander Dratan is a masterclass in quiet strength, projecting a calm determination that anchors the audience amid the film’s swirling uncertainties. Devon Hallard as the ship’s taciturn navigator and Tessa Reidan as a brilliant but cautious physicist round out a trio of characters whose emotional interplay infuses the narrative with tension and warmth. Each of them conveys layers of humanity through subtle gestures—a hesitant glance, a tremor in the voice—that resonate long after the closing credits. Even the secondary cast, including Martin Cender as the skeptical technician and Lina Acosta as the empathic communications officer, never feel like afterthoughts. They enrich the ensemble with their own struggles and revelations, ensuring that every whisper aboard the *Verion-9* matters.\n\n### Visual and Auditory Spectacle\n\nVisually, *Singularity’s Echo* is a landmark achievement. Under the guidance of effects designer Cavel Junt, the film weaves together dynamic particle simulations, refracted light displays, and fluid gravitational distortions that challenge viewers’ sense of stability. The climactic traversal into the heart of the anomaly is a symphony of color and form: starfields shimmer, corridors twist, and perspectives shift as if the audience itself is being gently folded and unfolded within a cosmic hand. Complementing these sights is a score by Hirolo Vaard that might be best described as liquid sound—notes flow and merge, layering alien whispers with distant echoes, enveloping the audience in a gentle, hypnotic tide. Together, these technical feats forge an immersive environment where wonder coexists with unease, and every frame hums with potential.\n\n### Conclusion: A Triumphant Addition to the Sci-Fi Canon\n\nThough *Singularity’s Echo* occasionally wavers under the weight of its own ambition—some may find the pacing challenging, and a handful of narrative threads remain tantalizingly unresolved—these imperfections are trivial compared to the film’s broader achievements. This is a story that invites us to contemplate the fragility of understanding and the boundless expanse of possibility. In an age of safe sequels and predictable plots, *Singularity’s Echo* dares to push beyond known stars, offering a transformative cinematic experience that lingers in the mind and senses long after the projector’s glow fades. It stands as a testament to the power of imagination, ensuring that audiences will look to the cosmos with renewed curiosity, eagerly awaiting whatever astonishing horizon lies beyond.", + "publishDate": "2024-10-07T19:00:00.000Z", + "featuredImage": { + "imageUrl": "https://boxel-images.boxel.ai/app-assets/blog-posts/space-movie-thumb.jpeg", + "credit": null, + "caption": null, + "altText": "", + "size": "cover", + "height": 485, + "width": null + }, + "description": "In an era hungry for originality, \"Singularity’s Echo\" emerges as a luminous beacon in the cosmic darkness—an audacious journey that redefines what viewers can expect from the genre. Directed by the elusive auteur Oruni Kilrain, this film harnesses the raw power of interstellar wonder and transforms it into a multilayered narrative tapestry. Blending heart-stirring drama with disorienting visuals, \"Singularity’s Echo* reminds us that true cinematic frontiers remain ripe for exploration, stretching beyond the gravitational pull of safe storytelling.", + "thumbnailURL": "https://boxel-images.boxel.ai/app-assets/blog-posts/space-movie-thumb.jpeg" + }, + "relationships": { + "authorBio": { + "links": { + "self": "../Author/3a655a91-98b5-4f33-a071-b62d39218b33" + } + }, + "blog": { + "links": { + "self": "../ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1" + } + }, + "editors": { + "links": { + "self": null + } + } + }, + "meta": { + "adoptsFrom": { + "module": "../review", + "name": "Review" + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1.json b/packages/experiments-realm/ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1.json new file mode 100644 index 0000000000..087c4493a4 --- /dev/null +++ b/packages/experiments-realm/ReviewBlog/583df6bb-5739-418a-9186-978bd72816c1.json @@ -0,0 +1,17 @@ +{ + "data": { + "type": "card", + "attributes": { + "website": "www.cinereview.com", + "title": "CineReview", + "description": null, + "thumbnailURL": null + }, + "meta": { + "adoptsFrom": { + "module": "../review-blog", + "name": "ReviewBlog" + } + } + } +} \ No newline at end of file diff --git a/packages/experiments-realm/blog-app.gts b/packages/experiments-realm/blog-app.gts index ddbc9de02e..a67aaa3b30 100644 --- a/packages/experiments-realm/blog-app.gts +++ b/packages/experiments-realm/blog-app.gts @@ -20,7 +20,6 @@ import { getCard, SupportedMimeType, type LooseSingleCardDocument, - relativeURL, } from '@cardstack/runtime-common'; import { type SortOption, @@ -225,6 +224,7 @@ class BlogAppTemplate extends Component { @title={{or @model.title ''}} @tagline={{or @model.description ''}} @thumbnailURL={{or @model.thumbnailURL ''}} + @icon={{@model.constructor.icon}} @element='header' aria-label='Sidebar Header' /> @@ -267,21 +267,19 @@ class BlogAppTemplate extends Component { <:grid> {{#if this.query}} -
- - <:meta as |card|> - {{#if this.showAdminData}} - - {{/if}} - - -
+ + <:meta as |card|> + {{#if this.showAdminData}} + + {{/if}} + + {{/if}} @@ -308,7 +306,7 @@ class BlogAppTemplate extends Component { - filters: LayoutFilter[] = new TrackedArray(FILTERS); + filters: LayoutFilter[] = new TrackedArray(this.args.model.filters); @tracked private selectedView: ViewOption = 'card'; @tracked private activeFilter: LayoutFilter = this.filters[0]; @@ -373,25 +371,13 @@ class BlogAppTemplate extends Component { }; filter.cardRef = cardRef; - let realmUrl = this.args.model[realmURL]; - if (!this.args.model.id || !realmUrl?.href) { - throw new Error(`Missing card id or realm url`); - } - let relativeTo = relativeURL( - new URL(this.args.model.id), - new URL(`${cardRef.module}/${cardRef.name}`), - realmUrl, - ); - if (!relativeTo) { - throw new Error(`Missing relative url`); + if (!this.args.model.id) { + throw new Error(`Missing card id`); } filter.query = { filter: { on: cardRef, - any: [ - { eq: { 'blog.id': this.args.model.id } }, - { eq: { 'blog.id': relativeTo } }, - ], + eq: { 'blog.id': this.args.model.id }, }, }; } @@ -451,12 +437,14 @@ export class BlogApp extends CardDef { static icon = BlogAppIcon; static prefersWideFormat = true; static headerColor = '#fff500'; + filters = FILTERS; static isolated = BlogAppTemplate; static fitted = class Fitted extends Component { diff --git a/packages/experiments-realm/review-blog.gts b/packages/experiments-realm/review-blog.gts index 25b42502f2..ffa841d7ae 100644 --- a/packages/experiments-realm/review-blog.gts +++ b/packages/experiments-realm/review-blog.gts @@ -1,6 +1,7 @@ import MovieIcon from '@cardstack/boxel-icons/movie'; import BlogPostIcon from '@cardstack/boxel-icons/newspaper'; import AuthorIcon from '@cardstack/boxel-icons/square-user'; +import CategoriesIcon from '@cardstack/boxel-icons/hierarchy-3'; import { type LayoutFilter } from './components/layout'; import { BlogApp } from './blog-app'; @@ -31,5 +32,15 @@ export class ReviewBlog extends BlogApp { module: new URL('./author', import.meta.url).href, }, }, + { + displayName: 'Categories', + icon: CategoriesIcon, + cardTypeName: 'Category', + createNewButtonText: 'Category', + cardRef: { + name: 'BlogCategory', + module: new URL('./blog-category', import.meta.url).href, + }, + }, ]; } diff --git a/packages/experiments-realm/review.gts b/packages/experiments-realm/review.gts index bf6259d1c1..86c2a41dcb 100644 --- a/packages/experiments-realm/review.gts +++ b/packages/experiments-realm/review.gts @@ -5,6 +5,7 @@ import { } from 'https://cardstack.com/base/card-api'; import { setBackgroundImage } from './components/layout'; import { formatDatetime } from './blog-app'; +import { categoryStyle } from './blog-category'; import { BlogPost } from './blog-post'; import { RatingsSummary } from './ratings-summary'; @@ -23,8 +24,15 @@ export class Review extends BlogPost { />
- {{! TODO: replace with category field }} - Movie Review + {{#if @model.categories.length}} +
+ {{#each @model.categories as |category|}} +
+ {{category.title}} +
+ {{/each}} +
+ {{/if}} {{#if @model.rating}} <@fields.rating class='rating-info' @format='atom' /> {{/if}} @@ -32,9 +40,7 @@ export class Review extends BlogPost {

<@fields.title />

{{@model.description}}

- {{#if @model.formattedAuthors}} - - {{/if}} + {{#if @model.datePublishedIsoTimestamp}}
@@ -206,12 +228,17 @@ export class Review extends BlogPost { display: flex; align-items: center; justify-content: space-between; - gap: var(--boxel-sp-sm); + flex-wrap: wrap; + gap: var(--boxel-sp-lg) var(--boxel-sp-sm); + } + .categories { + display: inline-flex; + gap: var(--boxel-sp-xxs); + flex-wrap: wrap; } .category { - background-color: var(--boxel-yellow); padding: 0 var(--boxel-sp-xs); - border-radius: 5px; + border-radius: var(--boxel-border-radius-sm); font: 500 var(--boxel-font-sm); letter-spacing: var(--boxel-lsp-xs); } @@ -263,8 +290,8 @@ export class Review extends BlogPost { .byline { display: inline-flex; align-items: center; - gap: 0 var(--boxel-sp-xxxs); - font-weight: 600; + gap: var(--boxel-sp-xs) var(--boxel-sp); + flex-wrap: wrap; } .author { display: contents; /* workaround for removing block-levelness of atom format */ From 884a2fd0838b1b94f98349c7e02293568f858ada Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Sat, 21 Dec 2024 19:11:40 +0000 Subject: [PATCH 53/99] lint --- packages/ai-bot/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai-bot/helpers.ts b/packages/ai-bot/helpers.ts index cedf617b80..1ff3d1455f 100644 --- a/packages/ai-bot/helpers.ts +++ b/packages/ai-bot/helpers.ts @@ -315,7 +315,7 @@ function getLastUserMessage( const lastMessage = userMessages[userMessages.length - 1]; if ( lastMessage.type === 'm.room.message' && - lastMessage.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE + lastMessage.content.msgtype === APP_BOXEL_MESSAGE_MSGTYPE ) { return lastMessage as CardMessageEvent; } From 138457a255be27264b5fa7791489166c75d6c5dd Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Sat, 21 Dec 2024 19:12:42 +0000 Subject: [PATCH 54/99] Update type in tests --- .../commands/send-ai-assistant-message-test.gts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts index fc84fc3a9c..1263514d34 100644 --- a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts +++ b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts @@ -66,7 +66,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { prompt: 'Hello, world!', }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'org.boxel.message'); + assert.strictEqual(message.content.msgtype, 'app.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 0); }); @@ -85,7 +85,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { commands: [{ command, autoExecute: false }], }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'org.boxel.message'); + assert.strictEqual(message.content.msgtype, 'app.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -109,7 +109,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: false, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'org.boxel.message'); + assert.strictEqual(message.content.msgtype, 'app.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -133,7 +133,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: true, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'org.boxel.message'); + assert.strictEqual(message.content.msgtype, 'app.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -161,7 +161,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: false, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'org.boxel.message'); + assert.strictEqual(message.content.msgtype, 'app.boxel.message'); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 2); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); From d8bee73404da787fdfb89473293ad830ba12b4be Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Sat, 21 Dec 2024 19:23:27 +0000 Subject: [PATCH 55/99] Update to use constant for msg types --- .../commands/send-ai-assistant-message-test.gts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts index 1263514d34..2fa0646805 100644 --- a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts +++ b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts @@ -3,6 +3,7 @@ import { RenderingTestContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; +import { APP_BOXEL_MESSAGE_MSGTYPE } from '@cardstack/runtime-common/matrix-constants'; import { Loader } from '@cardstack/runtime-common'; import SendAiAssistantMessageCommand from '@cardstack/host/commands/send-ai-assistant-message'; @@ -66,7 +67,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { prompt: 'Hello, world!', }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'app.boxel.message'); + assert.strictEqual(message.content.msgtype, APP_BOXEL_MESSAGE_MSGTYPE); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 0); }); @@ -85,7 +86,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { commands: [{ command, autoExecute: false }], }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'app.boxel.message'); + assert.strictEqual(message.content.msgtype, APP_BOXEL_MESSAGE_MSGTYPE); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -109,7 +110,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: false, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'app.boxel.message'); + assert.strictEqual(message.content.msgtype, APP_BOXEL_MESSAGE_MSGTYPE); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -133,7 +134,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: true, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'app.boxel.message'); + assert.strictEqual(message.content.msgtype, APP_BOXEL_MESSAGE_MSGTYPE); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 1); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); @@ -161,7 +162,7 @@ module('Integration | commands | send-ai-assistant-message', function (hooks) { requireCommandCall: false, }); let message = getRoomEvents(roomId).pop()!; - assert.strictEqual(message.content.msgtype, 'app.boxel.message'); + assert.strictEqual(message.content.msgtype, APP_BOXEL_MESSAGE_MSGTYPE); let boxelMessageData = JSON.parse(message.content.data); assert.strictEqual(boxelMessageData.context.tools.length, 2); assert.strictEqual(boxelMessageData.context.tools[0].type, 'function'); From ce95f7e7492fe9dc01277192afe5052ac9fbd85f Mon Sep 17 00:00:00 2001 From: Ian Calvert Date: Sat, 21 Dec 2024 20:27:14 +0000 Subject: [PATCH 56/99] Linting --- .../integration/commands/send-ai-assistant-message-test.gts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts index 2fa0646805..b492718483 100644 --- a/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts +++ b/packages/host/tests/integration/commands/send-ai-assistant-message-test.gts @@ -3,8 +3,8 @@ import { RenderingTestContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; -import { APP_BOXEL_MESSAGE_MSGTYPE } from '@cardstack/runtime-common/matrix-constants'; import { Loader } from '@cardstack/runtime-common'; +import { APP_BOXEL_MESSAGE_MSGTYPE } from '@cardstack/runtime-common/matrix-constants'; import SendAiAssistantMessageCommand from '@cardstack/host/commands/send-ai-assistant-message'; import SwitchSubmodeCommand from '@cardstack/host/commands/switch-submode'; From 47ea7c16102bc4b90cfd7c32a631d3f235319d7c Mon Sep 17 00:00:00 2001 From: tintinthong Date: Sat, 21 Dec 2024 19:15:32 +0800 Subject: [PATCH 57/99] enhance work tracker to use the new getCards --- .../src/components/drag-and-drop/index.gts | 57 +++-- .../productivity/task-cards-resource.gts | 129 ++++------- packages/experiments-realm/work-tracker.gts | 210 +++++++++--------- 3 files changed, 185 insertions(+), 211 deletions(-) diff --git a/packages/boxel-ui/addon/src/components/drag-and-drop/index.gts b/packages/boxel-ui/addon/src/components/drag-and-drop/index.gts index 07c2e305f1..f8f8ea7da7 100644 --- a/packages/boxel-ui/addon/src/components/drag-and-drop/index.gts +++ b/packages/boxel-ui/addon/src/components/drag-and-drop/index.gts @@ -13,6 +13,7 @@ export type DndItem = Record; export interface DndKanbanBoardArgs { columns: DndColumn[]; + displayCard?: (card: DndItem) => boolean; isLoading?: boolean; onMove?: ( draggedCard: DndItem, @@ -153,6 +154,14 @@ export default class DndKanbanBoard extends Component< this.draggedCard = null; } + @action + displayCard(card: DndItem): boolean { + if (this.args.displayCard) { + return this.args.displayCard(card); + } + return true; + } +