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
19 changes: 7 additions & 12 deletions libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
viewChild,
ElementRef,
ChangeDetectionStrategy,
ViewEncapsulation,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import type { StreamResourceRef } from '@cacheplane/stream-resource';
Expand Down Expand Up @@ -43,7 +42,6 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
DebugSummaryComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES],
template: `
<div class="flex h-full">
Expand All @@ -68,18 +66,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
</div>
</ng-template>

<!-- AI messages: no bubble, avatar + markdown -->
<!-- AI messages: avatar inline with content (ChatGPT pattern) -->
<ng-template chatMessageTemplate="ai" let-message>
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 flex items-center justify-center text-[11px] font-semibold shrink-0"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<span class="text-xs font-medium" style="color: var(--chat-text-muted);">Assistant</span>
</div>
<div class="flex gap-3">
<div
class="chat-md pl-8 break-words text-[length:var(--chat-font-size)] leading-[var(--chat-line-height)]"
class="w-7 h-7 flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<div
class="chat-md flex-1 min-w-0 break-words text-[length:var(--chat-font-size)] leading-[var(--chat-line-height)]"
style="color: var(--chat-text);"
[innerHTML]="renderMd(messageContent(message))"
></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
ChangeDetectionStrategy,
} from '@angular/core';
import type { StreamResourceRef } from '@cacheplane/stream-resource';
import { ICON_WARNING } from '../../styles/chat-icons';

export type InterruptAction = 'accept' | 'edit' | 'respond' | 'ignore';

Expand All @@ -24,7 +23,7 @@ export type InterruptAction = 'accept' | 'edit' | 'respond' | 'ignore';
>
<!-- Warning header -->
<div class="flex items-start gap-2">
<span style="color: var(--chat-warning-text);" [innerHTML]="warningIcon"></span>
<span style="color: var(--chat-warning-text);"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<div class="flex-1">
<h3 class="text-sm font-semibold m-0" style="color: var(--chat-warning-text);">Agent Interrupt</h3>
<p class="text-sm mt-1 mb-0" style="color: var(--chat-warning-text);">{{ interruptPayload() }}</p>
Expand Down Expand Up @@ -80,7 +79,6 @@ export class ChatInterruptPanelComponent {

readonly action = output<InterruptAction>();

readonly warningIcon = ICON_WARNING;

readonly interrupt = computed(() => this.ref().interrupt());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
ChangeDetectionStrategy,
} from '@angular/core';
import type { SubagentStreamRef } from '@cacheplane/stream-resource';
import { ICON_AGENT, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN } from '../../styles/chat-icons';

type SubagentStatus = 'pending' | 'running' | 'complete' | 'error';

Expand Down Expand Up @@ -36,7 +35,7 @@ export { statusColor };
aria-label="Toggle subagent details"
>
<div class="flex items-center gap-2">
<span style="color: var(--chat-text-muted);" [innerHTML]="agentIcon"></span>
<span style="color: var(--chat-text-muted);"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="10" rx="2"/><circle cx="12" cy="5" r="2"/><path d="M12 7v4"/><line x1="8" y1="16" x2="8" y2="16"/><line x1="16" y1="16" x2="16" y2="16"/></svg></span>
<span class="text-sm font-medium" [style.color]="'var(--chat-text)'">
Subagent
<span class="font-mono text-xs ml-1" style="color: var(--chat-text-muted);">{{ subagent().toolCallId }}</span>
Expand All @@ -46,7 +45,7 @@ export { statusColor };
{{ subagent().status() }}
</span>
</div>
<span class="text-xs" style="color: var(--chat-text-muted);"><span [innerHTML]="expanded() ? chevronUp : chevronDown"></span></span>
<span class="text-xs" style="color: var(--chat-text-muted);">@if (expanded()) {<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5L6 4.5L9 7.5"/></svg>} @else {<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5L6 7.5L9 4.5"/></svg>}</span>
</button>

<!-- Expanded content -->
Expand Down Expand Up @@ -76,9 +75,6 @@ export class ChatSubagentCardComponent {

readonly expanded = signal(false);

readonly agentIcon = ICON_AGENT;
readonly chevronUp = ICON_CHEVRON_UP;
readonly chevronDown = ICON_CHEVRON_DOWN;

readonly statusColor = computed(() => statusColor(this.subagent().status()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ICON_TOOL, ICON_CHECK, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN } from '../../styles/chat-icons';

export interface ToolCallInfo {
id: string;
Expand All @@ -29,13 +28,13 @@ export interface ToolCallInfo {
aria-label="Toggle tool call details"
>
<div class="flex items-center gap-2">
<span style="color: var(--chat-text-muted);" [innerHTML]="toolIcon"></span>
<span style="color: var(--chat-text-muted);"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg></span>
<span class="font-mono" [style.color]="'var(--chat-text)'">{{ toolCall().name }}</span>
@if (toolCall().result !== undefined) {
<span class="flex items-center gap-1 text-xs" style="color: var(--chat-success);"><span [innerHTML]="checkIcon"></span> done</span>
<span class="flex items-center gap-1 text-xs" style="color: var(--chat-success);"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 6L5 8.5L9.5 3.5"/></svg> done</span>
}
</div>
<span class="text-xs" style="color: var(--chat-text-muted);"><span [innerHTML]="expanded() ? chevronUp : chevronDown"></span></span>
<span class="text-xs" style="color: var(--chat-text-muted);">@if (expanded()) {<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7.5L6 4.5L9 7.5"/></svg>} @else {<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.5L6 7.5L9 4.5"/></svg>}</span>
</button>

<!-- Expanded content: inputs and outputs -->
Expand All @@ -61,10 +60,6 @@ export class ChatToolCallCardComponent {

readonly expanded = signal(false);

readonly toolIcon = ICON_TOOL;
readonly checkIcon = ICON_CHECK;
readonly chevronUp = ICON_CHEVRON_UP;
readonly chevronDown = ICON_CHEVRON_DOWN;

formatJson(value: unknown): string {
if (typeof value === 'string') return value;
Expand Down
19 changes: 7 additions & 12 deletions libs/chat/src/lib/compositions/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
ElementRef,
ChangeDetectionStrategy,
inject,
ViewEncapsulation,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import type { StreamResourceRef } from '@cacheplane/stream-resource';
Expand Down Expand Up @@ -39,7 +38,6 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
ChatThreadListComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES],
template: `
<div class="flex h-full overflow-hidden">
Expand Down Expand Up @@ -109,18 +107,15 @@ import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown
</div>
</ng-template>

<!-- AI messages: no bubble, avatar + markdown -->
<!-- AI messages: avatar inline with content (ChatGPT pattern) -->
<ng-template chatMessageTemplate="ai" let-message>
<div class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 flex items-center justify-center text-[11px] font-semibold shrink-0"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<span class="text-xs font-medium" style="color: var(--chat-text-muted);">Assistant</span>
</div>
<div class="flex gap-3">
<div
class="chat-md pl-8 break-words text-[length:var(--chat-font-size)] leading-[var(--chat-line-height)]"
class="w-7 h-7 flex items-center justify-center text-xs font-semibold shrink-0 mt-0.5"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<div
class="chat-md flex-1 min-w-0 break-words text-[length:var(--chat-font-size)] leading-[var(--chat-line-height)]"
style="color: var(--chat-text);"
[innerHTML]="renderMd(messageContent(message))"
></div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import type { StreamResourceRef } from '@cacheplane/stream-resource';
import { ICON_SEND } from '../../styles/chat-icons';

export function submitMessage(
ref: StreamResourceRef<any, any>,
Expand All @@ -34,7 +33,7 @@ export function submitMessage(
template: `
<form
(submit)="onSubmit(); $event.preventDefault()"
class="flex items-end gap-2 px-4 py-2.5 pl-[18px] border transition-colors duration-150"
class="flex items-center gap-2 px-4 py-2.5 pl-[18px] border transition-colors duration-150"
[style.background]="'var(--chat-input-bg)'"
[style.borderColor]="focused() ? 'var(--chat-input-focus-border)' : 'var(--chat-input-border)'"
[style.borderRadius]="'var(--chat-radius-input)'"
Expand Down Expand Up @@ -63,8 +62,10 @@ export function submitMessage(
[style.background]="'var(--chat-send-bg)'"
[style.color]="'var(--chat-send-text)'"
aria-label="Send message"
[innerHTML]="sendIcon"
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 12V4M8 4L4 8M8 4L12 8"/>
</svg>
</button>
</form>
`,
Expand All @@ -77,7 +78,6 @@ export class ChatInputComponent {
readonly messageText = signal<string>('');
readonly isDisabled = computed(() => this.ref().isLoading());
readonly focused = signal(false);
readonly sendIcon = ICON_SEND;

onSubmit(): void {
const submitted = submitMessage(this.ref(), this.messageText());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ import { MessageTemplateDirective } from './message-template.directive';
import type { MessageTemplateType } from '../../chat.types';

/**
* Maps a LangChain message `_getType()` string to a {@link MessageTemplateType}.
* Maps a LangChain message to a {@link MessageTemplateType}.
* Handles both class instances (with `_getType()`) and plain objects (with `type` property)
* since SSE stream events deliver plain JSON, not hydrated BaseMessage instances.
* Exported as a standalone function so it can be unit-tested without DOM rendering.
*/
export function getMessageType(message: BaseMessage): MessageTemplateType {
const type = message._getType();
// Try class method first, fall back to plain object property
const type = typeof message._getType === 'function'
? message._getType()
: (message as unknown as Record<string, unknown>)['type'] as string ?? 'ai';
switch (type) {
case 'human':
return 'human';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,15 @@ export function isTyping(ref: StreamResourceRef<any, any>): boolean {
`],
template: `
@if (visible()) {
<div role="status" aria-label="Agent is typing" class="flex flex-col gap-1.5">
<div class="flex items-center gap-2">
<div
class="w-6 h-6 flex items-center justify-center text-[11px] font-semibold shrink-0"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<span class="text-xs font-medium" style="color: var(--chat-text-muted);">Assistant</span>
<div class="flex items-center gap-1 pl-1">
<span class="chat-dot"></span>
<span class="chat-dot"></span>
<span class="chat-dot"></span>
</div>
<div role="status" aria-label="Agent is typing" class="flex items-center gap-3">
<div
class="w-7 h-7 flex items-center justify-center text-xs font-semibold shrink-0"
style="background: var(--chat-avatar-bg); color: var(--chat-avatar-text); border-radius: var(--chat-radius-avatar);"
>A</div>
<div class="flex items-center gap-1">
<span class="chat-dot"></span>
<span class="chat-dot"></span>
<span class="chat-dot"></span>
</div>
</div>
}
Expand Down
34 changes: 17 additions & 17 deletions libs/chat/src/lib/styles/chat-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,38 +52,38 @@ export function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHt
* Must be included in a component with ViewEncapsulation.None or via ::ng-deep.
*/
export const CHAT_MARKDOWN_STYLES = `
.chat-md p { margin: 0 0 0.75em; }
.chat-md p:last-child { margin-bottom: 0; }
.chat-md code {
:host ::ng-deep .chat-md p { margin: 0 0 0.75em; }
:host ::ng-deep .chat-md p:last-child { margin-bottom: 0; }
:host ::ng-deep .chat-md code {
background: var(--chat-bg-alt);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875em;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
}
.chat-md pre {
:host ::ng-deep .chat-md pre {
background: var(--chat-bg-alt);
padding: 12px 16px;
border-radius: var(--chat-radius-card);
overflow-x: auto;
margin: 0.75em 0;
}
.chat-md pre code { background: none; padding: 0; }
.chat-md ul, .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; }
.chat-md li { margin: 0.25em 0; }
.chat-md a { color: var(--chat-text); text-decoration: underline; }
.chat-md strong { font-weight: 600; }
.chat-md blockquote {
:host ::ng-deep .chat-md pre code { background: none; padding: 0; }
:host ::ng-deep .chat-md ul, :host ::ng-deep .chat-md ol { margin: 0.5em 0; padding-left: 1.5em; }
:host ::ng-deep .chat-md li { margin: 0.25em 0; }
:host ::ng-deep .chat-md a { color: var(--chat-text); text-decoration: underline; }
:host ::ng-deep .chat-md strong { font-weight: 600; }
:host ::ng-deep .chat-md blockquote {
border-left: 3px solid var(--chat-border);
padding-left: 12px;
margin: 0.75em 0;
color: var(--chat-text-muted);
}
.chat-md h1, .chat-md h2, .chat-md h3, .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; }
.chat-md h1 { font-size: 1.25em; }
.chat-md h2 { font-size: 1.125em; }
.chat-md h3 { font-size: 1em; }
.chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; }
.chat-md th, .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; }
.chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; }
:host ::ng-deep .chat-md h1, :host ::ng-deep .chat-md h2, :host ::ng-deep .chat-md h3, :host ::ng-deep .chat-md h4 { margin: 1em 0 0.5em; font-weight: 600; }
:host ::ng-deep .chat-md h1 { font-size: 1.25em; }
:host ::ng-deep .chat-md h2 { font-size: 1.125em; }
:host ::ng-deep .chat-md h3 { font-size: 1em; }
:host ::ng-deep .chat-md table { border-collapse: collapse; width: 100%; margin: 0.75em 0; }
:host ::ng-deep .chat-md th, :host ::ng-deep .chat-md td { border: 1px solid var(--chat-border); padding: 6px 12px; text-align: left; }
:host ::ng-deep .chat-md th { background: var(--chat-bg-alt); font-weight: 600; font-size: 0.875em; }
`;
Loading