Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat input area component #957

Merged
merged 9 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we produce enough of these maybe we can just do this 😆