<hlm-tabs tab="preview" class="mx-auto flex h-[calc(100vh-7.5rem)] max-w-6xl flex-col">
<hlm-tabs-list variant="line" class="shrink-0">
<button hlmTabsTrigger="preview">Preview</button>
<button hlmTabsTrigger="code">Code</button>
</hlm-tabs-list>
<div hlmTabsContent="preview" class="mt-3 min-h-0 flex-1">
<div
class="border-border bg-background flex h-full flex-col overflow-hidden rounded-lg border md:flex-row"
>
<!-- Conversation sidebar -->
<aside class="border-border hidden h-full min-h-0 w-64 shrink-0 flex-col border-r md:flex">
<div class="border-border flex items-center justify-between border-b px-3 py-2">
<span class="text-foreground text-sm font-medium">Conversations</span>
<button
hlmBtn
variant="ghost"
size="icon-sm"
type="button"
aria-label="New conversation"
(click)="newConversation()"
>
<ng-icon name="lucidePlus" class="text-[length:--spacing(3)]" />
</button>
</div>
<div class="border-border border-b px-2 py-2">
<div
class="bg-muted/40 focus-within:ring-ring flex items-center gap-2 rounded-md px-2 focus-within:ring-2"
>
<ng-icon
name="lucideSearch"
class="text-[length:--spacing(3)] text-muted-foreground shrink-0"
/>
<input
type="search"
[(ngModel)]="searchQueryInput"
(ngModelChange)="searchQuery.set($event)"
placeholder="Search chats..."
aria-label="Search conversations"
class="text-foreground placeholder:text-muted-foreground h-8 w-full bg-transparent text-sm outline-none"
/>
@if (searchQuery().length > 0) {
<button
type="button"
aria-label="Clear search"
class="text-muted-foreground hover:text-foreground shrink-0"
(click)="clearSearch()"
>
<ng-icon name="lucideX" class="text-[length:--spacing(3)]" />
</button>
}
</div>
</div>
@if (filteredConversations().length === 0) {
<p class="text-muted-foreground flex-1 px-3 py-6 text-center text-xs">
No conversations match "{{ searchQuery() }}"
</p>
} @else {
<pk-conversation-list
[conversations]="filteredConversations()"
[activeId]="selectedConvoId()"
(selected)="selectedConvoId.set($event)"
(renamed)="onRename($event)"
(deleted)="onDelete($event)"
class="min-h-0 flex-1"
/>
}
<div class="border-border border-t p-2">
<button
type="button"
[hlmDropdownMenuTrigger]="userMenu"
side="top"
align="start"
class="hover:bg-muted focus-visible:ring-ring flex w-full items-center gap-3 rounded-md p-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-2"
aria-label="Account menu"
>
<img
[src]="userAvatar"
alt=""
aria-hidden="true"
class="h-8 w-8 shrink-0 rounded-full object-cover"
/>
<div class="flex min-w-0 flex-1 flex-col">
<span class="text-foreground truncate text-sm font-medium">PianoNic</span>
<span class="text-muted-foreground truncate text-xs">Pro plan</span>
</div>
<ng-icon
name="lucideChevronsUpDown"
class="text-[length:--spacing(4)] text-muted-foreground shrink-0"
/>
</button>
<ng-template #userMenu>
<hlm-dropdown-menu class="min-w-[220px]">
<button hlmDropdownMenuItem type="button" (triggered)="onMenu('account')">
<ng-icon name="lucideUserRound" class="text-[length:--spacing(3)]" />
Account
</button>
<button hlmDropdownMenuItem type="button" (triggered)="onMenu('settings')">
<ng-icon name="lucideSettings" class="text-[length:--spacing(3)]" />
Settings
</button>
<hlm-dropdown-menu-separator />
<button
hlmDropdownMenuItem
variant="destructive"
type="button"
(triggered)="onMenu('signout')"
>
<ng-icon name="lucideLogOut" class="text-[length:--spacing(3)]" />
Sign out
</button>
</hlm-dropdown-menu>
</ng-template>
</div>
</aside>
<!-- Main chat column -->
<section class="flex h-full min-h-0 min-w-0 flex-1 flex-col">
<!-- Header -->
<header class="border-border flex items-center justify-between gap-3 border-b px-4 py-2">
<pk-model-selector [models]="models" [(value)]="selectedModelId" />
<span class="text-muted-foreground truncate text-xs">
{{ currentConvoTitle() }}
</span>
</header>
<!-- Message thread -->
<pk-chat-container-root #thread class="relative flex-1 px-4 py-4">
<pk-chat-container-content class="gap-6">
@for (m of currentMessages(); track m.id) {
@if (m.role === 'user') {
<div class="group flex max-w-[80%] flex-col items-end gap-1 self-end">
<div hlmMessage align="end">
<pk-message-edit
#userEditor
editTrigger="hidden"
[content]="m.content"
(saved)="onMessageSaved(m.id, $event)"
>
<div hlmMessageContent>
<div hlmBubble>
<div hlmBubbleContent class="whitespace-pre-wrap">{{ m.content }}</div>
</div>
</div>
</pk-message-edit>
</div>
@if (m.attachments?.length) {
<pk-attachment-preview [attachments]="m.attachments" [removable]="false" />
}
<pk-message-actions-bar
[actions]="userActions"
(actionPicked)="onUserAction($event, m, userEditor)"
/>
</div>
} @else {
<div class="group flex max-w-[85%] flex-col gap-1 self-start">
<div hlmMessage>
<div hlmMessageAvatar>
<hlm-avatar>
<span hlmAvatarFallback>AI</span>
</hlm-avatar>
</div>
<div hlmMessageContent>
<div hlmBubble variant="muted">
<div hlmBubbleContent>
@if (m.streaming) {
<pk-response-stream
mode="typewriter"
[speed]="60"
[textStream]="m.content"
(completed)="onStreamCompleted(m.id)"
/>
} @else if (m.markdown) {
<pk-markdown [content]="m.content" />
} @else {
{{ m.content }}
}
</div>
</div>
</div>
</div>
@if (!m.streaming) {
<pk-message-actions-bar
class="ml-11"
[actions]="assistantActionsFor(m.id)"
(actionPicked)="onAssistantAction($event, m)"
/>
}
</div>
}
}
@if (isStreaming()) {
<div class="flex items-center gap-3 pl-11">
<span class="shimmer text-muted-foreground text-sm font-medium" role="status">
Thinking...
</span>
</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="rounded-full transition-opacity"
[class.opacity-0]="thread.isAtBottom()"
[class.pointer-events-none]="thread.isAtBottom()"
(click)="thread.scrollToBottom()"
>
<ng-icon name="lucideChevronDown" class="text-[length:--spacing(4)]" />
</button>
</div>
</pk-chat-container-root>
<!-- Composer -->
<div class="px-4 pt-2 pb-4">
<div class="rounded-2xl border bg-background px-3 pt-2 pb-3">
<pk-composer
placeholder="Ask anything..."
[(value)]="inputValue"
[sendBlocked]="isStreaming()"
(submitted)="send()"
>
@if (stagedAttachments().length > 0) {
<pk-attachment-preview
pkComposerTop
[attachments]="stagedAttachments()"
(removed)="onStagedRemoved($event)"
/>
}
<button
pkComposerStart
hlmBtn
variant="ghost"
size="icon-sm"
type="button"
hlmTooltip="Attach files"
aria-label="Attach files"
(click)="addSampleAttachment()"
>
<ng-icon name="lucidePaperclip" class="text-[length:--spacing(4)]" />
</button>
<button
pkComposerStart
hlmBtn
variant="ghost"
size="icon-sm"
type="button"
hlmTooltip="Voice input"
aria-label="Voice input"
>
<ng-icon name="lucideMic" class="text-[length:--spacing(4)]" />
</button>
<pk-token-counter
pkComposerStart
class="ml-2"
display="compact"
[text]="inputValue()"
[limit]="2000"
/>
</pk-composer>
</div>
</div>
</section>
</div>
</div>
<div hlmTabsContent="code" class="mt-3 min-h-0 min-w-0 flex-1 overflow-hidden">
<div class="border-border bg-muted/30 h-full w-full overflow-auto rounded-lg border">
<pk-code-block class="block w-max min-w-full">
<pk-code-block-code [code]="fullChatHtmlSource" language="html" />
</pk-code-block>
</div>
</div>
</hlm-tabs>