All blocks
Block
Scrollable chat thread
Full back-and-forth thread inside a chat-container. Auto-sticks to the bottom on new turns; scroll up and a floating spartan button (reading the container's isAtBottom()) takes you back.
Auto-stick + back-to-bottom button
<pk-chat-container-root #thread class="relative h-full p-4">
<pk-chat-container-content class="gap-4">
@for (t of turns(); track t.id) {
@if (t.role === 'user') {
<div hlmMessage align="end">
<div hlmMessageContent>
<div hlmBubble>
<div hlmBubbleContent>{{ t.text }}</div>
</div>
</div>
</div>
} @else {
<div hlmMessage>
<div hlmMessageAvatar>
<hlm-avatar>
<span hlmAvatarFallback>AI</span>
</hlm-avatar>
</div>
<div hlmMessageContent>
<div hlmBubble variant="secondary">
<div hlmBubbleContent class="prose dark:prose-invert">
<pk-markdown [content]="t.text" />
</div>
</div>
</div>
</div>
}
}
</pk-chat-container-content>
<pk-chat-container-scroll-anchor />
<div class="sticky bottom-2 ml-auto w-fit pr-1">
<button
hlmBtn
variant="outline"
size="icon"
type="button"
aria-label="Scroll to bottom"
class="size-10 rounded-full transition-all duration-150 ease-out data-[hidden=true]:pointer-events-none data-[hidden=true]:translate-y-4 data-[hidden=true]:scale-95 data-[hidden=true]:opacity-0"
[attr.data-hidden]="thread.isAtBottom()"
(click)="thread.scrollToBottom()"
>
<ng-icon name="lucideChevronDown" />
</button>
</div>
</pk-chat-container-root>
// Component
protected readonly turns = signal<Turn[]>([
{ id: 1, role: 'user', text: '...' },
{ id: 2, role: 'assistant', text: '...' },
]);
protected addTurn(): void {
this.turns.update(list => [...list, newTurn]);
// chat-container auto-scrolls only if user is at the bottom
}