Skip to content
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
10 changes: 5 additions & 5 deletions apps/website/content/docs/chat/getting-started/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Required for the build toolchain and package installation.
## Install the package

```bash
npm install @ngaf/chat
npm install @ngaf/chat marked
```

That's it. The chat components ship with their own design tokens and component-encapsulated styles. **No PostCSS config, no global stylesheet import, no Tailwind required.**
`marked` is required for parsing assistant markdown. Beyond that: the chat components ship with their own design tokens and component-encapsulated styles. **No PostCSS config, no global stylesheet import, no Tailwind required.**

## Peer Dependencies

Expand All @@ -35,10 +35,10 @@ That's it. The chat components ship with their own design tokens and component-e
| `@ngaf/partial-json` | `^0.0.1` | Yes |
| `@json-render/core` | `^0.16.0` | Yes |
| `@langchain/core` | `^1.1.33` | Yes |
| `marked` | `^15.0.0 \|\| ^16.0.0` | Optional |
| `marked` | `^15.0.0 \|\| ^16.0.0` | Yes |

<Callout type="tip" title="Markdown rendering">
The `marked` package is optional. When installed, AI messages render as full markdown (headings, code blocks, tables, lists). Without it, the library falls back to plain text with `<br>` newline conversion.
<Callout type="info" title="Markdown rendering">
`marked` parses AI message content into HTML (headings, code blocks, tables, lists). It is a required peer; the library ships a defensive plain-text fallback for resilience, but the rendered output is unusable without `marked` installed.
</Callout>

## Configure provideChat() (optional)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ Angular 20+ project with an agent provider configured. See [Agent Installation](
<Step title="Install the package">

```bash
npm install @ngaf/chat
npm install @ngaf/chat marked
```

`marked` is the markdown renderer used for assistant messages.

</Step>
<Step title="Configure providers">

Expand Down
7 changes: 1 addition & 6 deletions libs/chat/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/chat",
"version": "0.0.3",
"version": "0.0.4",
"exports": {
".": {
"types": "./index.d.ts",
Expand All @@ -26,11 +26,6 @@
"rxjs": "~7.8.0",
"marked": "^15.0.0 || ^16.0.0"
},
"peerDependenciesMeta": {
"marked": {
"optional": true
}
},
"license": "MIT",
"repository": {
"type": "git",
Expand Down
16 changes: 11 additions & 5 deletions libs/chat/src/lib/compositions/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ import type { ChatRenderEvent } from './chat-render-event';
padding: 60px 20px;
color: var(--ngaf-chat-text-muted);
text-align: center;
flex: 1;
min-height: 0;
}
.chat-empty[hidden] { display: none; }
.chat-empty__title { font-size: 1.125rem; font-weight: 500; color: var(--ngaf-chat-text); margin: 0; }
.chat-empty__sub { margin: 0; font-size: var(--ngaf-chat-font-size-sm); }
.chat-empty__title { font-size: 1.125rem; font-weight: 500; color: var(--ngaf-chat-text); margin: 0; }
.chat-empty__sub { margin: 0; font-size: var(--ngaf-chat-font-size-sm); }
.chat-scroll { flex: 1; min-height: 0; overflow-y: auto; }
Expand All @@ -85,11 +90,12 @@ import type { ChatRenderEvent } from './chat-render-event';
<chat-window>
<ng-content select="[chatHeader]" chatHeader />
<div chatBody class="chat-scroll" #scrollContainer>
@if (agent().messages().length === 0 && !agent().isLoading()) {
<div class="chat-empty">
<ng-content select="[chatEmptyState]" />
</div>
}
<div class="chat-empty" [hidden]="agent().messages().length !== 0 || agent().isLoading()">
<ng-content select="[chatEmptyState]">
<p class="chat-empty__title">How can I help?</p>
<p class="chat-empty__sub">Ask anything to get started.</p>
</ng-content>
</div>

<chat-message-list [agent]="agent()">
<ng-template chatMessageTemplate="human" let-message let-i="index">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export function submitMessage(
<textarea
#textareaEl
class="chat-input__textarea"
[(ngModel)]="messageTextProxy"
[ngModel]="messageText()"
(ngModelChange)="messageText.set($event)"
name="messageText"
[placeholder]="placeholder()"
[disabled]="isDisabled()"
Expand Down Expand Up @@ -83,9 +84,7 @@ export class ChatInputComponent {
readonly isDisabled = computed(() => this.agent().isLoading());
readonly focused = signal(false);

/** Two-way binding helper for ngModel */
get messageTextProxy(): string { return this.messageText(); }
set messageTextProxy(v: string) { this.messageText.set(v); }


readonly canSubmit = computed(() => {
if (this.isDisabled()) return false;
Expand Down
36 changes: 17 additions & 19 deletions libs/chat/src/lib/primitives/chat-message/chat-message.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,17 @@ export type ChatMessageRole = 'user' | 'assistant' | 'system' | 'tool';
host: {
'[attr.data-role]': 'role()',
'[attr.data-current]': 'currentStr()',
'[attr.data-streaming]': 'streamingStr()',
'[attr.data-prev-role]': 'prevRole() ?? null',
},
template: `
@switch (role()) {
@case ('user') {
<div class="chat-message__bubble"><ng-content /></div>
}
@case ('assistant') {
<div class="chat-message__assistant-body">
<ng-content />
@if (streaming() && current()) {
<span class="chat-message__caret" aria-hidden="true">▍</span>
}
</div>
<div class="chat-message__controls">
<ng-content select="[chatMessageControls]" />
</div>
}
@default {
<div><ng-content /></div>
}
}
<div [class]="bodyClass()">
<ng-content />
<span class="chat-message__caret" aria-hidden="true">▍</span>
</div>
<div class="chat-message__controls">
<ng-content select="[chatMessageControls]" />
</div>
`,
})
export class ChatMessageComponent {
Expand All @@ -45,4 +34,13 @@ export class ChatMessageComponent {
readonly prevRole = input<ChatMessageRole | undefined>(undefined);

readonly currentStr = computed(() => String(this.current()));
readonly streamingStr = computed(() => String(this.streaming()));

readonly bodyClass = computed(() => {
switch (this.role()) {
case 'user': return 'chat-message__bubble';
case 'assistant': return 'chat-message__assistant-body';
default: return 'chat-message__plain';
}
});
}
12 changes: 10 additions & 2 deletions libs/chat/src/lib/styles/chat-message.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,31 @@ export const CHAT_MESSAGE_STYLES = `
}

.chat-message__caret {
display: inline-block;
display: none;
margin-left: 2px;
width: 0.6ch;
color: var(--ngaf-chat-text-muted);
animation: ngaf-chat-caret-blink 1.2s step-end infinite;
}
:host([data-role="assistant"][data-current="true"][data-streaming="true"]) .chat-message__caret {
display: inline-block;
}

.chat-message__plain { /* system / tool fallback */ }

.chat-message__controls {
display: none;
position: absolute;
left: 0;
bottom: -28px;
display: flex;
gap: 1rem;
opacity: 0;
transition: opacity 200ms ease;
pointer-events: none;
}
:host([data-role="assistant"]) .chat-message__controls {
display: flex;
}
:host([data-role="assistant"]:hover) .chat-message__controls,
:host([data-role="assistant"]:focus-within) .chat-message__controls,
:host([data-current="true"]) .chat-message__controls {
Expand Down
Loading