Skip to content

Commit

Permalink
Chat input area component (#957)
Browse files Browse the repository at this point in the history
* Chat input component using BoxelInput

* change height with text

* adjust padding

* lint fix

* more padding adjustment

* use onKeyModifier to submit messages via cmd+Enter or ctrl+Enter

* update description

* remove artifacts
  • Loading branch information
burieberry authored Jan 10, 2024
1 parent 5bcb536 commit ed11ef8
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/boxel-ui/addon/raw-icons/send.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/boxel-ui/addon/src/icons.gts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import IconX from './icons/icon-x.gts';
import LoadingIndicator from './icons/loading-indicator.gts';
import Profile from './icons/profile.gts';
import Search from './icons/search.gts';
import Send from './icons/send.gts';
import Sparkle from './icons/sparkle.gts';
import SuccessBordered from './icons/success-bordered.gts';
import ThreeDotsHorizontal from './icons/three-dots-horizontal.gts';
Expand Down Expand Up @@ -73,6 +74,7 @@ export const ALL_ICON_COMPONENTS = [
LoadingIndicator,
Profile,
Search,
Send,
Sparkle,
SuccessBordered,
ThreeDotsHorizontal,
Expand Down Expand Up @@ -112,6 +114,7 @@ export {
LoadingIndicator,
Profile,
Search,
Send,
Sparkle,
SuccessBordered,
ThreeDotsHorizontal,
Expand Down
23 changes: 23 additions & 0 deletions packages/boxel-ui/addon/src/icons/send.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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<Signature> = <template>
<svg
xmlns='http://www.w3.org/2000/svg'
width='20'
height='20'
viewBox='0 0 20 20'
...attributes
><path
d='m31.329 39.777a1.047 1.047 0 0 0 1.306 1.084s-.392.111.048-.013a10 10 0 1 0 -5.344.006c.433.121.026.007.026.007a1.052 1.052 0 0 0 1.306-1.084v-8.487c0-.187-.105-.232-.236-.1 0 0-1.078 1.1-1.465 1.5a1.341 1.341 0 1 1 -1.891-1.9l4.07-4.114a1.17 1.17 0 0 1 1.658 0l4.113 4.119a1.341 1.341 0 0 1 -1.879 1.905c-.4-.411-1.476-1.509-1.476-1.509-.131-.133-.236-.089-.236.1z'
fill='var(--icon-color, #000)'
fill-rule='evenodd'
transform='translate(-20 -20.91)'
/></svg>
</template>;

// @ts-expect-error this is the only way to set a name on a Template Only Component currently
IconComponent.name = 'SendIcon';
export default IconComponent;
99 changes: 99 additions & 0 deletions packages/host/app/components/ai-assistant/chat-input/index.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { on } from '@ember/modifier';
import { action } from '@ember/object';
import Component from '@glimmer/component';

import onKeyMod from 'ember-keyboard/modifiers/on-key';

import { BoxelInput, IconButton } from '@cardstack/boxel-ui/components';
import { Send } from '@cardstack/boxel-ui/icons';
import { setCssVar } from '@cardstack/boxel-ui/modifiers';

interface Signature {
Element: HTMLDivElement;
Args: {
value: string;
onInput: (val: string) => void;
onSend: (val: string) => void;
};
}

export default class AiAssistantChatInput extends Component<Signature> {
<template>
<div class='chat-input-container'>
<label for='ai-chat-input' class='boxel-sr-only'>
Enter text to chat with AI Assistant
</label>
<BoxelInput
class='chat-input'
@id='ai-chat-input'
@type='textarea'
@value={{@value}}
@onInput={{@onInput}}
@placeholder='Enter text here'
{{onKeyMod 'cmd+Enter' this.onSend}}
{{onKeyMod 'ctrl+Enter' this.onSend}}
{{setCssVar chat-input-height=this.height}}
...attributes
/>
<IconButton
{{on 'click' this.onSend}}
class='send-button'
@icon={{Send}}
@height='20'
@width='20'
>
Send
</IconButton>
</div>
<style>
.chat-input-container {
display: grid;
grid-template-columns: 1fr auto;
padding: var(--boxel-sp-xs);
background-color: var(--boxel-light);
}
.chat-input {
height: var(--chat-input-height);
border-color: transparent;
font-weight: 500;
padding: var(--boxel-sp-xxs);
resize: none;
}
.chat-input::placeholder {
color: var(--boxel-400);
}
.chat-input:hover:not(:disabled) {
border-color: var(--boxel-border-color);
}
.send-button {
--icon-color: var(--boxel-highlight);
height: 30px;
padding-bottom: var(--boxel-sp-xs);
}
.send-button:hover:not(:disabled) {
--icon-color: var(--boxel-highlight-hover);
}
</style>
</template>

@action onSend() {
this.args.onSend(this.args.value);
}

get height() {
const lineHeight = 20;
const padding = 9;

let lineCount = (this.args.value.match(/\n/g) ?? []).length + 1;
let count = 2;

if (lineCount > 5) {
count = 5;
} else if (lineCount > 2) {
count = lineCount;
}

let height = count * lineHeight + 2 * padding;
return `${height}px`;
}
}
53 changes: 53 additions & 0 deletions packages/host/app/components/ai-assistant/chat-input/usage.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { fn } from '@ember/helper';
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import FreestyleUsage from 'ember-freestyle/components/freestyle/usage';

import AiAssistantChatInput from './index';

export default class AiAssistantChatInputUsage extends Component {
@tracked value = '';

@action onSend(message: string) {
console.log(`message sent: ${message}`);
}

<template>
<FreestyleUsage @name='AiAssistant::ChatInput'>
<:description>
Chat input field for AI Assistant is a \`BoxelInput\` component of type
'textarea' with a send button. This component accepts all arguments that
are accepted by \`BoxelInput\` component in addition to an \`onSend\`
argument for action to take when message is submitted. A message can be
submitted via pressing \`cmd+Enter\` or \`ctrl+Enter\` keys or by
clicking on the send button.
</:description>
<:example>
<AiAssistantChatInput
@value={{this.value}}
@onInput={{fn (mut this.value)}}
@onSend={{this.onSend}}
/>
</:example>
<:api as |Args|>
<Args.String
@name='value'
@description='Chat input field'
@onInput={{fn (mut this.value)}}
@value={{this.value}}
/>
<Args.Action
@name='onInput'
@description='Action to be called when input is entered'
/>
<Args.Action
@name='onSend'
@description='Action to be called when "cmd+Enter" or \`ctrl+Enter\` keys are pressed or send button is clicked'
@value={{this.onSend}}
/>
</:api>
</FreestyleUsage>
</template>
}
2 changes: 2 additions & 0 deletions packages/host/app/templates/host-freestyle.gts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import FreestyleSection from 'ember-freestyle/components/freestyle-section';
import { pageTitle } from 'ember-page-title';
import RouteTemplate from 'ember-route-template';

import AiAssistantChatInputUsage from '@cardstack/host/components/ai-assistant/chat-input/usage';
import AiAssistantMessageUsage from '@cardstack/host/components/ai-assistant/message/usage';
import ProfileAvatarIconVisualUsage from '@cardstack/host/components/operator-mode/profile-avatar-icon/usage';
import SearchSheetUsage from '@cardstack/host/components/search-sheet/usage';
Expand All @@ -29,6 +30,7 @@ class HostFreestyleComponent extends Component<HostFreestyleSignature> {
get usageComponents() {
return [
['AiAssistant::Message', AiAssistantMessageUsage],
['AiAssistant::ChatInput', AiAssistantChatInputUsage],
['ProfileAvatarIconVisualUsage', ProfileAvatarIconVisualUsage],
['SearchSheet', SearchSheetUsage],
].map(([name, c]) => {
Expand Down
15 changes: 15 additions & 0 deletions packages/host/types/ember-keyboard/modifiers/on-key.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Modifier from 'ember-modifier';

interface Signature {
Element: HTMLElement;
Args: {
Positional: [keyCombo: string, callback?: (ev: KeyboardEvent) => void];
Named: {
event?: string;
activated?: boolean;
priority?: number;
};
};
}

export default class OnKeyModifier extends Modifier<Signature> {}

0 comments on commit ed11ef8

Please sign in to comment.