diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 2a4a7b524..221d785b8 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -1575,6 +1575,12 @@ "description": "", "optional": false }, + { + "name": "currentAssistantStreaming", + "type": "Signal", + "description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `agent().isLoading() && i === agent().messages().length - 1`\n `i === agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).", + "optional": false + }, { "name": "forkRequested", "type": "OutputEmitterRef", diff --git a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts index 8e5b477f7..270e02631 100644 --- a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts +++ b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts @@ -1,17 +1,32 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; -import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; +import { ChatComponent, ChatWelcomeSuggestionComponent, a2uiBasicCatalog } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; +const WELCOME_SUGGESTIONS = [ + { label: 'LAX → JFK', value: 'I want to fly LAX to JFK' }, + { label: 'SFO → SEA', value: 'I want to fly SFO to SEA' }, +] as const; + @Component({ selector: 'app-a2ui', standalone: true, - imports: [ChatComponent, ExampleChatLayoutComponent], + imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent], template: ` - + +
+ @for (s of suggestions; track s.value) { + + } +
+
`, }) @@ -21,4 +36,9 @@ export class A2uiComponent { assistantId: environment.a2uiAssistantId, }); protected readonly catalog = a2uiBasicCatalog(); + protected readonly suggestions = WELCOME_SUGGESTIONS; + + protected send(text: string): void { + void this.agent.submit({ message: text }); + } } diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 1984804b2..692e43fed 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -255,7 +255,12 @@ export function isPinned( - @if (pinned()) { + + @if (pinned() && !currentAssistantStreaming()) { } @@ -395,6 +400,29 @@ export class ChatComponent { private programmaticScrollCount = 0; private static readonly PIN_TOLERANCE_PX = 150; + /** + * True iff there's a current (last-index) assistant message that's + * still streaming. The bubble's own caret already signals loading; + * we suppress the floor typing-indicator in that case so the user + * doesn't see two loading affordances at once. + * + * Matches the same `streaming + current` condition the bubble uses + * to enable `.chat-message__caret`: + * `agent().isLoading() && i === agent().messages().length - 1` + * `i === agent().messages().length - 1` + * + * Restricted to assistant role because the caret only renders on + * assistant bubbles (`:host([data-role="assistant"][data-current=... + * ][data-streaming=...])`). + */ + protected readonly currentAssistantStreaming = computed(() => { + if (!this.agent().isLoading()) return false; + const msgs = this.agent().messages(); + if (msgs.length === 0) return false; + const last = msgs[msgs.length - 1]; + return last?.role === 'assistant'; + }); + constructor() { // Inject the chat lib's root CSS custom properties (--ngaf-chat-bg, // --ngaf-chat-surface, --ngaf-chat-radius-input, etc.) the first