diff --git a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts index 16bffd58d..84935a841 100644 --- a/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts +++ b/cockpit/deep-agents/filesystem/angular/src/app/filesystem.component.ts @@ -1,89 +1,22 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -interface ToolCallEntry { - name: string; - args: string; - result?: string; -} - /** * FilesystemComponent demonstrates agent file operations. * - * The agent can read and write files using tool calls. The sidebar - * shows a real-time log of each file operation as it happens. - * - * Key integration points: - * - `stream.messages()` contains all messages including tool call results - * - `computed()` derives tool call entries from AI messages - * - Tool calls update reactively as the agent performs file operations + * The agent can read and write files using tool calls. */ @Component({ selector: 'app-filesystem', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

File Operations

- @for (entry of toolCallEntries(); track $index) { -
- - {{ entry.name === 'read_file' ? 'πŸ“–' : '✏️' }} - -
-
- {{ getFilePath(entry.args) }} -
-
- {{ entry.name === 'read_file' ? 'read' : 'write' }} - {{ entry.result ? ' Β· done' : ' Β· running…' }} -
-
-
- } - @empty { -

Ask the agent to read or write a file.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class FilesystemComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - toolCallEntries = computed(() => { - const msg = this.stream.messages(); - const calls: ToolCallEntry[] = []; - for (const m of msg) { - if ((m as any).tool_calls) { - for (const tc of (m as any).tool_calls) { - calls.push({ name: tc.name, args: JSON.stringify(tc.args), result: tc.output }); - } - } - } - return calls; - }); - - getFilePath(args: string): string { - try { - const parsed = JSON.parse(args); - return parsed.path ?? args; - } catch { - return args; - } - } - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/filesystem/angular/src/styles.css b/cockpit/deep-agents/filesystem/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/filesystem/angular/src/styles.css +++ b/cockpit/deep-agents/filesystem/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts index c6c60fdf9..035fcfd8f 100644 --- a/cockpit/deep-agents/memory/angular/src/app/memory.component.ts +++ b/cockpit/deep-agents/memory/angular/src/app/memory.component.ts @@ -1,5 +1,5 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -7,51 +7,17 @@ import { environment } from '../environments/environment'; * MemoryComponent demonstrates persistent agent memory across sessions. * * The agent extracts facts about the user from each conversation turn - * and stores them in `agent_memory` state. The sidebar shows all learned - * facts in real time as the agent updates its memory. - * - * Key integration points: - * - `stream.value()` contains the agent state including `agent_memory` - * - `computed()` derives key/value pairs for the sidebar - * - Memory entries update reactively as the agent learns new facts + * and stores them in `agent_memory` state. */ @Component({ selector: 'app-da-memory', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Learned Facts

- @for (entry of memoryEntries(); track entry[0]) { -
-
{{ entry[0] }}
-
{{ entry[1] }}
-
- } - @empty { -

Tell the agent something about yourself to see it remember.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class MemoryComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - memoryEntries = computed(() => { - const val = this.stream.value() as { agent_memory?: Record } | undefined; - return Object.entries(val?.agent_memory ?? {}); - }); - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/memory/angular/src/styles.css b/cockpit/deep-agents/memory/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/memory/angular/src/styles.css +++ b/cockpit/deep-agents/memory/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts index 6394149e9..53c2b6b04 100644 --- a/cockpit/deep-agents/planning/angular/src/app/planning.component.ts +++ b/cockpit/deep-agents/planning/angular/src/app/planning.component.ts @@ -1,65 +1,23 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -interface PlanStep { - title: string; - status: 'pending' | 'running' | 'complete'; -} - /** * PlanningComponent demonstrates agent task decomposition. * * The agent receives a complex task, breaks it into ordered steps, - * and executes them. The sidebar shows each step's status in real time. - * - * Key integration points: - * - `stream.value()` contains the plan state with step list - * - `computed()` derives the plan steps for the sidebar - * - Steps update reactively as the agent works through them + * and executes them sequentially. */ @Component({ selector: 'app-planning', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Task Plan

- @for (step of planSteps(); track $index) { -
- - - {{ step.title }} - -
- } - @empty { -

Ask a complex question to see the plan.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class PlanningComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - planSteps = computed(() => { - const val = this.stream.value() as { plan?: PlanStep[] } | undefined; - return val?.plan ?? []; - }); - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/planning/angular/src/styles.css b/cockpit/deep-agents/planning/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/planning/angular/src/styles.css +++ b/cockpit/deep-agents/planning/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts index ab8e8ab75..cfcfec045 100644 --- a/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts +++ b/cockpit/deep-agents/sandboxes/angular/src/app/sandboxes.component.ts @@ -1,96 +1,23 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -interface ExecutionLog { - code: string; - stdout: string; - exitStatus: number; -} - /** * SandboxesComponent demonstrates a coding agent that executes Python code. * * The agent writes and runs code snippets to solve problems using a - * `run_code` tool. The sidebar shows execution logs β€” code input, stdout - * output, and exit status β€” for each sandbox execution. - * - * Key integration points: - * - `stream.messages()` contains all messages including tool call results - * - `computed()` derives execution log entries from tool calls in AI messages - * - Logs update reactively as the agent writes and runs code + * `run_code` tool. */ @Component({ selector: 'app-sandboxes', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Execution Logs

- @for (log of executionLogs(); track $index) { -
-
- - exit {{ log.exitStatus }} - -
-
{{ log.code }}
- @if (log.stdout) { -
stdout
-
{{ log.stdout }}
- } -
- } - @empty { -

Ask the agent to write and run Python code.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class SandboxesComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - executionLogs = computed(() => { - const msgs = this.stream.messages(); - const logs: ExecutionLog[] = []; - for (const m of msgs) { - if ((m as any).tool_calls) { - for (const tc of (m as any).tool_calls) { - if (tc.name === 'run_code' && tc.output) { - try { - const parsed = JSON.parse(tc.output); - logs.push({ - code: tc.args?.code ?? '', - stdout: parsed.stdout ?? '', - exitStatus: parsed.exit_status ?? 0, - }); - } catch { - logs.push({ - code: tc.args?.code ?? '', - stdout: tc.output, - exitStatus: 0, - }); - } - } - } - } - } - return logs; - }); - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/sandboxes/angular/src/styles.css b/cockpit/deep-agents/sandboxes/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/sandboxes/angular/src/styles.css +++ b/cockpit/deep-agents/sandboxes/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts index 718ce244f..f194e2185 100644 --- a/cockpit/deep-agents/skills/angular/src/app/skills.component.ts +++ b/cockpit/deep-agents/skills/angular/src/app/skills.component.ts @@ -1,92 +1,23 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; -interface SkillInvocation { - skillName: string; - args: string; - result?: string; -} - /** * SkillsComponent demonstrates a multi-skill agent with specialized tools. * * The agent can calculate math expressions, count words, and summarize text - * by selecting the appropriate skill tool for each user request. The sidebar - * shows each skill invocation as a card with the skill name, input args, - * and result. - * - * Key integration points: - * - `stream.messages()` contains all messages including tool call data - * - `computed()` derives skill invocation cards from tool calls in AI messages - * - Invocations update reactively as the agent calls and receives tool results + * by selecting the appropriate skill tool for each user request. */ @Component({ selector: 'app-skills', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Skill Invocations

- @for (inv of skillInvocations(); track $index) { -
-
- - {{ inv.skillName }} - - @if (inv.result) { - done - } @else { - running… - } -
-
- {{ inv.args }} -
- @if (inv.result) { -
- {{ inv.result }} -
- } -
- } - @empty { -

Ask the agent to calculate, count words, or summarize text.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class SkillsComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - skillInvocations = computed(() => { - const msgs = this.stream.messages(); - const invocations: SkillInvocation[] = []; - for (const m of msgs) { - if ((m as any).tool_calls) { - for (const tc of (m as any).tool_calls) { - invocations.push({ - skillName: tc.name, - args: JSON.stringify(tc.args), - result: tc.output, - }); - } - } - } - return invocations; - }); - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/skills/angular/src/styles.css b/cockpit/deep-agents/skills/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/skills/angular/src/styles.css +++ b/cockpit/deep-agents/skills/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts index 1868c0a6a..e7bf766a7 100644 --- a/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts +++ b/cockpit/deep-agents/subagents/angular/src/app/subagents.component.ts @@ -1,5 +1,5 @@ -import { Component, computed } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { Component } from '@angular/core'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -7,61 +7,17 @@ import { environment } from '../environments/environment'; * SubagentsComponent demonstrates the Deep Agents subagent delegation pattern. * * The orchestrator agent receives a task and delegates subtasks to specialist - * subagents via tool calls. Each tool call spawns a child agent that streams - * its own progress independently. - * - * Key integration points: - * - `stream.subagents()` returns a Map - * - `subagentEntries` derives a sorted array for sidebar rendering - * - Each entry shows the tool call ID (truncated), status badge, and message count - * - Subagent statuses update reactively: pending β†’ running β†’ complete + * subagents via tool calls. */ @Component({ selector: 'app-subagents', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Subagents

- @for (entry of subagentEntries(); track entry[0]) { -
-
- - {{ entry[1].status() }} - - - {{ entry[0].slice(0, 8) }}… - -
-
- {{ entry[1].messages().length }} message{{ entry[1].messages().length === 1 ? '' : 's' }} -
-
- } - @empty { -

Ask a question to see subagent activity.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class SubagentsComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - subagentEntries = computed(() => Array.from(this.stream.subagents().entries())); - - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } } diff --git a/cockpit/deep-agents/subagents/angular/src/styles.css b/cockpit/deep-agents/subagents/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/deep-agents/subagents/angular/src/styles.css +++ b/cockpit/deep-agents/subagents/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/deployment-runtime/angular/src/styles.css b/cockpit/langgraph/deployment-runtime/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/deployment-runtime/angular/src/styles.css +++ b/cockpit/langgraph/deployment-runtime/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts index 2aa442d12..5e692ca39 100644 --- a/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts +++ b/cockpit/langgraph/durable-execution/angular/src/app/durable-execution.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -8,89 +8,19 @@ import { environment } from '../environments/environment'; * with `streamResource()`. * * This example shows how a graph checkpoints at each node, enabling it to - * resume after failures. The sidebar shows execution status in real time: - * - `stream.status()` as a badge (idle/loading/resolved/error) - * - `stream.hasValue()` indicator for received data - * - A "Retry" button that calls `stream.reload()` when `stream.error()` is set - * - * The backend processes each request through three nodes: - * analyze β†’ plan β†’ generate - * Each node updates `state.step` so the UI can track progress. + * resume after failures. The backend processes each request through three + * nodes: analyze, plan, generate. Each node updates `state.step` so the + * UI can track progress. */ @Component({ selector: 'app-durable-execution', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Execution Status

- -
- Status -
- - {{ stream.status() }} - -
-
- -
- Data Received -
- - {{ stream.hasValue() ? 'Yes' : 'No' }} -
-
- - @if (stream.error()) { -
-
Execution Failed
- -
- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class DurableExecutionComponent { - /** - * The streaming resource backing this durable-execution demo. - * - * The graph runs three nodes (analyze β†’ plan β†’ generate), checkpointing - * after each one. If the graph fails partway through, `stream.reload()` - * re-submits the last input so the run can resume from the last checkpoint. - */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - /** - * Submit a message to be processed through the multi-node graph. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } - - /** - * Returns a colour for the status badge based on the current stream status. - */ - statusBadgeColor(): string { - switch (this.stream.status()) { - case 'loading': - case 'reloading': return '#2563eb'; - case 'resolved': return '#16a34a'; - case 'error': return '#dc2626'; - default: return '#6b7280'; - } - } } diff --git a/cockpit/langgraph/durable-execution/angular/src/styles.css b/cockpit/langgraph/durable-execution/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/durable-execution/angular/src/styles.css +++ b/cockpit/langgraph/durable-execution/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/interrupts/angular/src/styles.css b/cockpit/langgraph/interrupts/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/interrupts/angular/src/styles.css +++ b/cockpit/langgraph/interrupts/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/memory/angular/src/styles.css b/cockpit/langgraph/memory/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/memory/angular/src/styles.css +++ b/cockpit/langgraph/memory/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts index aa7f77f42..dabe6861c 100644 --- a/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts +++ b/cockpit/langgraph/persistence/angular/src/app/persistence.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -19,30 +19,8 @@ import { environment } from '../environments/environment'; @Component({ selector: 'app-persistence', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

Threads

- @for (id of threadIds; track id) { - - } - -
-
- `, + imports: [ChatComponent], + template: ``, }) export class PersistenceComponent { /** @@ -54,35 +32,5 @@ export class PersistenceComponent { protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, - onThreadId: (id: string) => { - this.currentThreadId = id; - if (!this.threadIds.includes(id)) this.threadIds.push(id); - }, }); - - threadIds: string[] = []; - currentThreadId = ''; - - /** - * Submit a message to the current thread. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } - - /** - * Switch to an existing thread, loading its full message history. - */ - selectThread(id: string): void { - this.currentThreadId = id; - this.stream.switchThread(id); - } - - /** - * Start a new conversation thread. - */ - newThread(): void { - this.currentThreadId = ''; - this.stream.switchThread(null); - } } diff --git a/cockpit/langgraph/persistence/angular/src/styles.css b/cockpit/langgraph/persistence/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/persistence/angular/src/styles.css +++ b/cockpit/langgraph/persistence/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/streaming/angular/src/styles.css b/cockpit/langgraph/streaming/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/streaming/angular/src/styles.css +++ b/cockpit/langgraph/streaming/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/subgraphs/angular/src/styles.css b/cockpit/langgraph/subgraphs/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/subgraphs/angular/src/styles.css +++ b/cockpit/langgraph/subgraphs/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts index 17759117d..96f92c31c 100644 --- a/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts +++ b/cockpit/langgraph/time-travel/angular/src/app/time-travel.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { LegacyChatComponent } from '@cacheplane/chat'; +import { ChatComponent } from '@cacheplane/chat'; import { streamResource } from '@cacheplane/stream-resource'; import { environment } from '../environments/environment'; @@ -7,78 +7,19 @@ import { environment } from '../environments/environment'; * TimeTravelComponent demonstrates replaying and branching conversation history. * * Key integration points: - * - `stream.history()` β€” array of ThreadState snapshots - * - `stream.branch()` β€” current branch identifier - * - `stream.setBranch(id)` β€” switch to a different checkpoint + * - `stream.history()` -- array of ThreadState snapshots + * - `stream.branch()` -- current branch identifier + * - `stream.setBranch(id)` -- switch to a different checkpoint */ @Component({ selector: 'app-time-travel', standalone: true, - imports: [LegacyChatComponent], - template: ` - - -

History

- @for (state of stream.history(); track $index) { - - } - @if (stream.history().length === 0) { -

No history yet. Send a message to begin.

- } -
-
- `, + imports: [ChatComponent], + template: ``, }) export class TimeTravelComponent { - /** - * The streaming resource with checkpointing enabled. - * - * `stream.history()` provides an array of ThreadState snapshots for - * the current thread. `stream.branch()` tracks the active checkpoint. - * Call `stream.setBranch(checkpointId)` to replay from a past state. - */ protected readonly stream = streamResource({ apiUrl: environment.langGraphApiUrl, assistantId: environment.streamingAssistantId, }); - - /** - * Submit a message to the current thread. - */ - send(text: string): void { - this.stream.submit({ messages: [{ role: 'human', content: text }] }); - } - - /** - * Branch the conversation from the selected checkpoint. - * After calling setBranch, the next submit will fork from that point. - */ - selectCheckpoint(state: { checkpoint_id?: string }): void { - if (state.checkpoint_id) { - this.stream.setBranch(state.checkpoint_id); - } - } - - /** - * Format a checkpoint for display in the sidebar. - */ - formatCheckpoint(state: { checkpoint_id?: string; created_at?: string }): string { - const id = state.checkpoint_id ?? 'unknown'; - const short = id.substring(0, 8); - if (state.created_at) { - const ts = new Date(state.created_at).toLocaleTimeString(); - return `${short}... @ ${ts}`; - } - return `${short}...`; - } } diff --git a/cockpit/langgraph/time-travel/angular/src/styles.css b/cockpit/langgraph/time-travel/angular/src/styles.css index 39a32d4e7..061c66cf8 100644 --- a/cockpit/langgraph/time-travel/angular/src/styles.css +++ b/cockpit/langgraph/time-travel/angular/src/styles.css @@ -1,5 +1,6 @@ @import "../../../../../libs/design-tokens/src/lib/tokens.css"; @import "tailwindcss"; +@source "../../../../../libs/chat/src/"; @theme { --color-bg: var(--ds-bg); diff --git a/docs/superpowers/plans/2026-04-06-chat-polish.md b/docs/superpowers/plans/2026-04-06-chat-polish.md new file mode 100644 index 000000000..c26fe1507 --- /dev/null +++ b/docs/superpowers/plans/2026-04-06-chat-polish.md @@ -0,0 +1,1110 @@ +# Chat Library Ship-Readiness Polish β€” Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix all 19 audit issues in `@cacheplane/chat` β€” convert all components to Tailwind, consolidate duplicated theme CSS, add auto-scroll, textarea auto-expand, markdown rendering, empty state, responsive sidebar, SVG icons, ARIA, and clean up the public API. The library must build and ship correctly via ng-packagr. + +**Architecture:** All components use Tailwind utility classes with `[var(--chat-*)]` arbitrary values for theme-aware styling. CSS custom properties are defined once in a shared TypeScript constant and imported by composition components. ng-packagr preserves Tailwind class names in compiled templates; the consuming app's Tailwind build generates the CSS. `marked` is added as a peer dep for markdown rendering. + +**Tech Stack:** Angular 20+, Tailwind CSS v4, `marked` (markdown), ng-packagr, Vitest + +**Parallelism:** Tasks 2–5 are independent and can be dispatched as parallel subagents after Task 1 completes. Task 6 depends on Tasks 2+3. Task 7 is final. + +--- + +## File Structure + +### New files +- `libs/chat/src/lib/styles/chat-theme.ts` β€” Shared CSS custom property definitions (replaces `chat-theme.css`) +- `libs/chat/src/lib/styles/chat-icons.ts` β€” SVG icon constants (replaces emoji) +- `libs/chat/src/lib/styles/chat-markdown.ts` β€” Markdown rendering utility + prose styles + +### Modified files +- `libs/chat/src/lib/compositions/chat/chat.component.ts` β€” Tailwind + auto-scroll + empty state + responsive sidebar +- `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts` β€” Tailwind + auto-scroll + shared theme +- `libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts` β€” Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts` β€” Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts` β€” Tailwind + SVG icons +- `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts` β€” Theme-aware colors +- `libs/chat/src/lib/primitives/chat-input/chat-input.component.ts` β€” Tailwind + auto-expand + focused signal +- `libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts` β€” Tailwind +- `libs/chat/src/lib/primitives/chat-error/chat-error.component.ts` β€” CSS var colors +- `libs/chat/src/lib/provide-chat.ts` β€” Typed ChatConfig +- `libs/chat/src/public-api.ts` β€” Remove legacy, add new exports +- `libs/chat/package.json` β€” Add marked peer dep +- `cockpit/*/angular/src/styles.css` (14 files) β€” Add @source for chat library + +### Deleted files +- `libs/chat/src/lib/styles/chat-theme.css` β€” Replaced by `chat-theme.ts` +- `libs/chat/src/lib/chat.component.ts` β€” Legacy `cp-chat` component +- `libs/chat/src/lib/chat-input.component.ts` β€” Legacy input component +- `libs/chat/src/lib/chat-message.component.ts` β€” Legacy message component + +--- + +## Task 1: Theme System Consolidation + +**Fixes:** Blocker #3 (theme CSS triplicated 6x), Medium #11 (CSS var fallback inconsistency) + +**Files:** +- Create: `libs/chat/src/lib/styles/chat-theme.ts` +- Create: `libs/chat/src/lib/styles/chat-icons.ts` +- Create: `libs/chat/src/lib/styles/chat-markdown.ts` +- Delete: `libs/chat/src/lib/styles/chat-theme.css` + +- [ ] **Step 1: Create `chat-theme.ts` with shared CSS custom property definitions** + +```typescript +// libs/chat/src/lib/styles/chat-theme.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +const DARK = ` + --chat-bg: #171717; + --chat-bg-alt: #222222; + --chat-bg-hover: #2a2a2a; + --chat-text: #e0e0e0; + --chat-text-muted: #777777; + --chat-text-placeholder: #666666; + --chat-border: #333333; + --chat-border-light: #2a2a2a; + --chat-user-bg: #2a2a2a; + --chat-user-text: #f5f5f5; + --chat-user-border: #333333; + --chat-avatar-bg: #333333; + --chat-avatar-text: #aaaaaa; + --chat-input-bg: #222222; + --chat-input-border: #333333; + --chat-input-focus-border: #555555; + --chat-send-bg: #444444; + --chat-send-text: #aaaaaa; + --chat-error-bg: #2d1515; + --chat-error-text: #f87171; + --chat-warning-bg: #2d2315; + --chat-warning-text: #fbbf24; + --chat-success: #4ade80; + --chat-radius-message: 20px; + --chat-radius-input: 24px; + --chat-radius-card: 12px; + --chat-radius-avatar: 8px; + --chat-max-width: 720px; + --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; + --chat-font-size: 15px; + --chat-line-height: 1.6; +`; + +const LIGHT = ` + --chat-bg: #ffffff; + --chat-bg-alt: #f5f5f5; + --chat-bg-hover: #ebebeb; + --chat-text: #1a1a1a; + --chat-text-muted: #999999; + --chat-text-placeholder: #999999; + --chat-border: #e5e5e5; + --chat-border-light: #f0f0f0; + --chat-user-bg: #f0f0f0; + --chat-user-text: #1a1a1a; + --chat-user-border: transparent; + --chat-avatar-bg: #f0f0f0; + --chat-avatar-text: #666666; + --chat-input-bg: #f5f5f5; + --chat-input-border: #e5e5e5; + --chat-input-focus-border: #cccccc; + --chat-send-bg: #e5e5e5; + --chat-send-text: #999999; + --chat-error-bg: #fef2f2; + --chat-error-text: #dc2626; + --chat-warning-bg: #fffbeb; + --chat-warning-text: #d97706; + --chat-success: #16a34a; +`; + +/** + * Shared theme styles for chat composition components. + * Defines CSS custom properties on :host for dark/light mode. + * Import into any composition's `styles` array. + */ +export const CHAT_THEME_STYLES = ` + :host { + ${DARK} + font-family: var(--chat-font-family); + font-size: var(--chat-font-size); + line-height: var(--chat-line-height); + color: var(--chat-text); + background: var(--chat-bg); + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + } + @media (prefers-color-scheme: light) { + :host:not([data-chat-theme="dark"]) { ${LIGHT} } + } + :host([data-chat-theme="light"]) { ${LIGHT} } +`; +``` + +- [ ] **Step 2: Create `chat-icons.ts` with SVG icon constants** + +Replaces emoji characters (βš™ ⚠ πŸ€– β–² β–Ό βœ“) with inline SVG strings for consistent cross-platform rendering. + +```typescript +// libs/chat/src/lib/styles/chat-icons.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 + +/** Chevron down (β–Ό replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_DOWN = ``; + +/** Chevron up (β–² replacement). 12x12, stroke-based. */ +export const ICON_CHEVRON_UP = ``; + +/** Gear icon (βš™ replacement). 14x14. */ +export const ICON_TOOL = ``; + +/** Warning triangle (⚠ replacement). 18x18. */ +export const ICON_WARNING = ``; + +/** Robot/agent icon (πŸ€– replacement). 14x14. */ +export const ICON_AGENT = ``; + +/** Check mark (βœ“ replacement). 12x12. */ +export const ICON_CHECK = ``; + +/** Send arrow (for chat input). 16x16. */ +export const ICON_SEND = ``; +``` + +- [ ] **Step 3: Create `chat-markdown.ts` with markdown rendering utility** + +```typescript +// libs/chat/src/lib/styles/chat-markdown.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { SecurityContext } from '@angular/core'; +import type { DomSanitizer, SafeHtml } from '@angular/platform-browser'; + +let markedParse: ((src: string) => string) | null = null; +let markedLoaded = false; + +function loadMarked(): void { + if (markedLoaded) return; + markedLoaded = true; + try { + // Dynamic require β€” marked is an optional peer dep + const m = require('marked'); + markedParse = (src: string) => m.marked.parse(src, { async: false }) as string; + } catch { + markedParse = null; + } +} + +/** + * Renders markdown content to sanitized HTML. + * Falls back to plain text with newlineβ†’br conversion if `marked` is not installed. + */ +export function renderMarkdown(content: string, sanitizer: DomSanitizer): SafeHtml { + loadMarked(); + if (markedParse) { + const html = markedParse(content); + return sanitizer.bypassSecurityTrustHtml( + sanitizer.sanitize(SecurityContext.HTML, html) ?? '' + ); + } + // Fallback: escape HTML and convert newlines to
+ const escaped = content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + return sanitizer.bypassSecurityTrustHtml(escaped); +} + +/** + * CSS for styling rendered markdown HTML. + * Uses .chat-md class prefix to avoid global conflicts. + * 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 { + 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 { + 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 { + 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; } +`; +``` + +- [ ] **Step 4: Delete `chat-theme.css`** + +```bash +rm libs/chat/src/lib/styles/chat-theme.css +``` + +- [ ] **Step 5: Run tests to verify no regressions** + +```bash +npx nx test chat +``` + +Expected: All 112 tests still pass (no component imports chat-theme.css directly). + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/styles/ +git commit -m "feat(chat): consolidate theme into shared TS module, add icons + markdown utils" +``` + +--- + +## Task 2: ChatComponent Overhaul + +**Fixes:** Blocker #4 (auto-scroll), High #9 (empty state), High #10 (responsive sidebar), Medium #14 (ARIA) + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat/chat.component.ts` +- Test: `libs/chat/src/lib/compositions/chat/chat.component.spec.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatComponent with Tailwind, auto-scroll, empty state, responsive sidebar, ARIA, and markdown** + +Replace the entire file: + +```typescript +// libs/chat/src/lib/compositions/chat/chat.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + input, + output, + signal, + computed, + effect, + viewChild, + ElementRef, + ChangeDetectionStrategy, + inject, + ViewEncapsulation, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; +import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; +import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; +import { ChatInputComponent } from '../../primitives/chat-input/chat-input.component'; +import { ChatTypingIndicatorComponent } from '../../primitives/chat-typing-indicator/chat-typing-indicator.component'; +import { ChatErrorComponent } from '../../primitives/chat-error/chat-error.component'; +import { ChatInterruptComponent } from '../../primitives/chat-interrupt/chat-interrupt.component'; +import { ChatThreadListComponent, Thread } from '../../primitives/chat-thread-list/chat-thread-list.component'; +import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; + +@Component({ + selector: 'chat', + standalone: true, + imports: [ + ChatMessagesComponent, + MessageTemplateDirective, + ChatInputComponent, + ChatTypingIndicatorComponent, + ChatErrorComponent, + ChatInterruptComponent, + ChatThreadListComponent, + ], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], + template: ` +
+ + @if (threads().length > 0) { + + } + + +
+ +
+
+ @if (ref().messages().length === 0 && !ref().isLoading()) { + +
+
A
+

Send a message to start a conversation.

+
+ } + + + + +
+
{{ messageContent(message) }}
+
+
+ + + +
+
+
A
+ Assistant +
+
+
+
+ + + +
{{ messageContent(message) }}
+
+ + + +
+ + {{ messageContent(message) }} + +
+
+
+ + +
+
+ + + + +
+

Agent paused: {{ interrupt.value }}

+
+
+
+ + +
+ +
+ + +
+
+ +
+
+
+
+ `, +}) +export class ChatComponent { + private readonly sanitizer = inject(DomSanitizer); + + readonly ref = input.required>(); + readonly threads = input([]); + readonly activeThreadId = input(''); + readonly threadSelected = output(); + readonly sidebarOpen = signal(false); + + readonly messageContent = messageContent; + + private readonly scrollContainer = viewChild>('scrollContainer'); + + /** Track message count to trigger auto-scroll */ + private readonly messageCount = computed(() => this.ref().messages().length); + + constructor() { + // Auto-scroll to bottom when new messages arrive or loading state changes + effect(() => { + this.messageCount(); // track + this.ref().isLoading(); // track + const el = this.scrollContainer()?.nativeElement; + if (el) { + // Use setTimeout to run after render + setTimeout(() => el.scrollTop = el.scrollHeight, 0); + } + }); + } + + renderMd(content: string) { + return renderMarkdown(content, this.sanitizer); + } +} +``` + +- [ ] **Step 2: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. The spec tests use `createMockStreamResourceRef()` which doesn't depend on template rendering. + +- [ ] **Step 3: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat/chat.component.ts +git commit -m "feat(chat): convert ChatComponent to Tailwind, add auto-scroll + empty state + responsive sidebar" +``` + +--- + +## Task 3: ChatDebugComponent Overhaul + +**Fixes:** Blocker #4 (auto-scroll), High #7 (duplicated templates), and theme-awareness for all debug sub-components + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-detail.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-controls.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatDebugComponent with shared theme + Tailwind + auto-scroll + markdown** + +Same pattern as ChatComponent (Task 2) but with the debug panel. Key changes: +- Import `CHAT_THEME_STYLES` from shared module (eliminating the copy-pasted CSS vars) +- Import `CHAT_MARKDOWN_STYLES` and `renderMarkdown` +- Add `ViewEncapsulation.None` for markdown styles +- Add auto-scroll via `viewChild` + `effect()` +- Use Tailwind classes for all layout +- Use `[var(--chat-*)]` arbitrary values for theme colors +- Add `role="log"` and `aria-live="polite"` to messages area + +The message templates should be identical to ChatComponent's templates (same 4 ng-template blocks). This is intentional β€” compositions co-locate their templates (shadcn model). The template code is the same as Task 2 Step 1. + +- [ ] **Step 2: Convert debug-timeline.component.ts to use theme vars** + +Replace hardcoded Tailwind colors with theme-var-based arbitrary values: +- `bg-blue-500` β†’ `bg-[var(--chat-success)]` (selected indicator) +- `border-blue-500` β†’ `border-[var(--chat-success)]` +- `bg-white` β†’ `bg-[var(--chat-bg)]` +- `border-gray-300` β†’ `border-[var(--chat-border)]` +- `bg-gray-200` β†’ `bg-[var(--chat-border)]` (rail) +- Keep layout Tailwind classes (`relative`, `space-y-1`, `absolute`, etc.) + +- [ ] **Step 3: Convert debug-checkpoint-card.component.ts to use theme vars** + +Replace hardcoded colors: +- `border-blue-400 bg-blue-50` β†’ `border-[var(--chat-input-focus-border)] bg-[var(--chat-bg-hover)]` +- `border-gray-200 bg-white hover:bg-gray-50` β†’ `border-[var(--chat-border)] bg-[var(--chat-bg)] hover:bg-[var(--chat-bg-hover)]` +- `text-gray-700` β†’ `text-[var(--chat-text)]` +- `bg-gray-100 text-gray-500` β†’ `bg-[var(--chat-bg-alt)] text-[var(--chat-text-muted)]` + +- [ ] **Step 4: Convert debug-state-diff.component.ts to use theme vars** + +Replace hardcoded colors: +- `bg-green-50 text-green-700` β†’ `bg-[var(--chat-bg-alt)] text-[var(--chat-success)]` +- `bg-red-50 text-red-700` β†’ `bg-[var(--chat-error-bg)] text-[var(--chat-error-text)]` +- `bg-amber-50 text-amber-700` β†’ `bg-[var(--chat-warning-bg)] text-[var(--chat-warning-text)]` +- `text-gray-400` β†’ `text-[var(--chat-text-muted)]` +- `text-gray-500` β†’ `text-[var(--chat-text-muted)]` + +- [ ] **Step 5: Convert debug-detail.component.ts to use theme vars** + +Replace: +- `text-gray-500` β†’ `text-[var(--chat-text-muted)]` + +- [ ] **Step 6: Convert debug-state-inspector.component.ts to use theme vars** + +Replace: +- `text-gray-700` β†’ `text-[var(--chat-text)]` + +- [ ] **Step 7: Convert debug-controls.component.ts to use theme vars** + +Replace: +- `bg-gray-100 hover:bg-gray-200` β†’ `bg-[var(--chat-bg-alt)] hover:bg-[var(--chat-bg-hover)]` +- `text-gray-500` β†’ `text-[var(--chat-text-muted)]` + +- [ ] **Step 8: Convert debug-summary.component.ts to use theme vars** + +Replace: +- `text-gray-500` β†’ `text-[var(--chat-text-muted)]` + +- [ ] **Step 9: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 10: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-debug/ +git commit -m "feat(chat): convert ChatDebug + sub-components to theme-aware Tailwind" +``` + +--- + +## Task 4: Primitives Overhaul + +**Fixes:** Blocker #1 (ChatError Tailwind), High #5 (textarea auto-expand), High #8 (focused signal) + +**Files:** +- Modify: `libs/chat/src/lib/primitives/chat-input/chat-input.component.ts` +- Modify: `libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts` +- Modify: `libs/chat/src/lib/primitives/chat-error/chat-error.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatInputComponent with Tailwind, auto-expand textarea, and focused signal** + +```typescript +// libs/chat/src/lib/primitives/chat-input/chat-input.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + output, + signal, + ChangeDetectionStrategy, +} 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, + text: string, +): string | null { + const trimmed = text.trim(); + if (!trimmed) return null; + ref.submit({ messages: [{ role: 'human', content: trimmed }] }); + return trimmed; +} + +@Component({ + selector: 'chat-input', + standalone: true, + imports: [FormsModule], + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + textarea { + field-sizing: content; + } + `], + template: ` + + `, +}) +export class ChatInputComponent { + readonly ref = input.required>(); + readonly submitOnEnter = input(true); + readonly placeholder = input(''); + readonly submitted = output(); + readonly messageText = signal(''); + readonly isDisabled = computed(() => this.ref().isLoading()); + readonly focused = signal(false); + readonly sendIcon = ICON_SEND; + + onSubmit(): void { + const submitted = submitMessage(this.ref(), this.messageText()); + if (submitted !== null) { + this.submitted.emit(submitted); + this.messageText.set(''); + } + } + + onKeydown(event: KeyboardEvent): void { + if (this.submitOnEnter() && !event.shiftKey) { + event.preventDefault(); + this.onSubmit(); + } + } +} +``` + +Key changes: +- `focused` is now a `signal(false)` (fixes OnPush change detection) +- Textarea uses `field-sizing: content` CSS for auto-expand (modern browsers) +- All inline styles replaced with Tailwind classes + CSS var bindings +- ARIA labels added +- SVG icon replaces inline SVG string + +- [ ] **Step 2: Rewrite ChatTypingIndicatorComponent with Tailwind** + +```typescript +// libs/chat/src/lib/primitives/chat-typing-indicator/chat-typing-indicator.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + ChangeDetectionStrategy, +} from '@angular/core'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; + +export function isTyping(ref: StreamResourceRef): boolean { + return ref.isLoading(); +} + +@Component({ + selector: 'chat-typing-indicator', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + styles: [` + .chat-dot { + display: inline-block; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--chat-text-muted); + animation: chat-dot-pulse 1.4s ease-in-out infinite; + } + .chat-dot:nth-child(2) { animation-delay: 0.2s; } + .chat-dot:nth-child(3) { animation-delay: 0.4s; } + @keyframes chat-dot-pulse { + 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); } + 40% { opacity: 1; transform: scale(1); } + } + `], + template: ` + @if (visible()) { +
+
+
A
+ Assistant +
+ + + +
+
+
+ } + `, +}) +export class ChatTypingIndicatorComponent { + readonly ref = input.required>(); + readonly visible = computed(() => this.ref().isLoading()); +} +``` + +- [ ] **Step 3: Rewrite ChatErrorComponent β€” remove Tailwind classes, use CSS vars** + +```typescript +// libs/chat/src/lib/primitives/chat-error/chat-error.component.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { + Component, + computed, + input, + ChangeDetectionStrategy, +} from '@angular/core'; +import type { StreamResourceRef } from '@cacheplane/stream-resource'; + +export function extractErrorMessage(error: unknown): string | null { + if (!error) return null; + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + return String(error); +} + +@Component({ + selector: 'chat-error', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (errorMessage(); as msg) { + + } + `, +}) +export class ChatErrorComponent { + readonly ref = input.required>(); + readonly errorMessage = computed(() => extractErrorMessage(this.ref().error())); +} +``` + +Note: `px-4 py-3 text-sm` are standard Tailwind layout classes that work with the consuming app's Tailwind build. The color/bg/radius use CSS vars via inline style. This is the correct hybrid approach. + +- [ ] **Step 4: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add libs/chat/src/lib/primitives/ +git commit -m "feat(chat): convert primitives to Tailwind, add textarea auto-expand + focused signal" +``` + +--- + +## Task 5: Compositions Overhaul + +**Fixes:** Blocker #2 (TimelineSlider unstyled without Tailwind), Medium #12 (emoji icons) + +**Files:** +- Modify: `libs/chat/src/lib/compositions/chat-interrupt-panel/chat-interrupt-panel.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-tool-call-card/chat-tool-call-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-subagent-card/chat-subagent-card.component.ts` +- Modify: `libs/chat/src/lib/compositions/chat-timeline-slider/chat-timeline-slider.component.ts` + +**Depends on:** Task 1 + +- [ ] **Step 1: Rewrite ChatInterruptPanelComponent with Tailwind + SVG icon** + +Replace `⚠` emoji with `ICON_WARNING` from chat-icons.ts. Convert inline styles to Tailwind + CSS var bindings. Add ARIA roles. + +```typescript +// Key changes in template: +// - Replace ⚠ with: +// +// - Convert all style="" attributes to Tailwind classes +// - Add role="alert" to the container +// In class: +// readonly warningIcon = ICON_WARNING; +``` + +- [ ] **Step 2: Rewrite ChatToolCallCardComponent with Tailwind + SVG icons** + +Replace `βš™` with `ICON_TOOL`, `βœ“` with `ICON_CHECK`, `β–²`/`β–Ό` with `ICON_CHEVRON_UP`/`ICON_CHEVRON_DOWN`. Convert inline styles to Tailwind + CSS var bindings. + +```typescript +// Key changes: +// - Import ICON_TOOL, ICON_CHECK, ICON_CHEVRON_UP, ICON_CHEVRON_DOWN +// - Replace emoji spans with [innerHTML]="icon" spans +// - Convert all style="" to Tailwind classes +// - Add aria-expanded to button (already exists) +// - Add aria-label="Toggle tool call details" to button +``` + +- [ ] **Step 3: Rewrite ChatSubagentCardComponent with Tailwind + SVG icon** + +Replace `πŸ€–` with `ICON_AGENT`. Convert inline styles to Tailwind + CSS var bindings. + +- [ ] **Step 4: Rewrite ChatTimelineSliderComponent with theme vars** + +This component already uses Tailwind but with hardcoded colors. Convert to theme-var-based: +- `border-blue-300 bg-blue-50` β†’ `border-[var(--chat-input-focus-border)] bg-[var(--chat-bg-hover)]` +- `border-gray-200 bg-white hover:bg-gray-50` β†’ `border-[var(--chat-border)] bg-[var(--chat-bg)] hover:bg-[var(--chat-bg-hover)]` +- `bg-blue-600 text-white` β†’ `bg-[var(--chat-send-bg)] text-[var(--chat-send-text)]` (selected indicator) +- `bg-gray-200 text-gray-500` β†’ `bg-[var(--chat-bg-alt)] text-[var(--chat-text-muted)]` (unselected) +- `text-gray-500` β†’ `text-[var(--chat-text-muted)]` +- `text-gray-400` β†’ `text-[var(--chat-text-muted)]` +- `text-gray-700` β†’ `text-[var(--chat-text)]` +- `bg-blue-100 text-blue-700 hover:bg-blue-200` β†’ `bg-[var(--chat-bg-alt)] text-[var(--chat-text)] hover:bg-[var(--chat-bg-hover)]` +- `bg-purple-100 text-purple-700 hover:bg-purple-200` β†’ `bg-[var(--chat-bg-alt)] text-[var(--chat-text)] hover:bg-[var(--chat-bg-hover)]` + +- [ ] **Step 5: Run tests** + +```bash +npx nx test chat +``` + +Expected: All tests pass. + +- [ ] **Step 6: Commit** + +```bash +git add libs/chat/src/lib/compositions/chat-interrupt-panel/ libs/chat/src/lib/compositions/chat-tool-call-card/ libs/chat/src/lib/compositions/chat-subagent-card/ libs/chat/src/lib/compositions/chat-timeline-slider/ +git commit -m "feat(chat): convert remaining compositions to Tailwind with SVG icons + theme vars" +``` + +--- + +## Task 6: API Cleanup + Build Verification + +**Fixes:** Medium #13 (selector prefix), Medium #15 (legacy export), Medium #16 (provideChat no-op), Low #19 (getMessageType fallthrough) + +**Files:** +- Modify: `libs/chat/src/public-api.ts` +- Modify: `libs/chat/src/lib/provide-chat.ts` +- Modify: `libs/chat/src/lib/chat.types.ts` +- Modify: `libs/chat/package.json` +- Delete: `libs/chat/src/lib/chat.component.ts` (legacy cp-chat) +- Delete: `libs/chat/src/lib/chat-input.component.ts` (legacy) +- Delete: `libs/chat/src/lib/chat-message.component.ts` (legacy) + +**Depends on:** Tasks 2–5 + +- [ ] **Step 1: Remove legacy component exports from public-api.ts** + +Remove this line from `libs/chat/src/public-api.ts`: +```typescript +// DELETE: export { ChatComponent as LegacyChatComponent } from './lib/chat.component'; +``` + +Add new exports: +```typescript +export { CHAT_THEME_STYLES } from './lib/styles/chat-theme'; +export { CHAT_MARKDOWN_STYLES, renderMarkdown } from './lib/styles/chat-markdown'; +export { + ICON_CHEVRON_DOWN, ICON_CHEVRON_UP, ICON_TOOL, + ICON_WARNING, ICON_AGENT, ICON_CHECK, ICON_SEND, +} from './lib/styles/chat-icons'; +``` + +- [ ] **Step 2: Delete legacy component files** + +```bash +rm libs/chat/src/lib/chat.component.ts +rm libs/chat/src/lib/chat-input.component.ts +rm libs/chat/src/lib/chat-message.component.ts +``` + +- [ ] **Step 3: Update ChatConfig in provide-chat.ts** + +```typescript +// libs/chat/src/lib/provide-chat.ts +// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +import { InjectionToken, makeEnvironmentProviders } from '@angular/core'; +import type { AngularRegistry } from '@cacheplane/render'; + +export interface ChatConfig { + /** Default render registry for generative UI components. */ + renderRegistry?: AngularRegistry; + /** Override the default AI avatar label (default: "A"). */ + avatarLabel?: string; + /** Override the default assistant display name (default: "Assistant"). */ + assistantName?: string; +} + +export const CHAT_CONFIG = new InjectionToken('CHAT_CONFIG'); + +export function provideChat(config: ChatConfig) { + return makeEnvironmentProviders([ + { provide: CHAT_CONFIG, useValue: config }, + ]); +} +``` + +- [ ] **Step 4: Add `marked` as optional peer dep in package.json** + +Edit `libs/chat/package.json` β€” add to peerDependencies and peerDependenciesMeta: + +```json +{ + "peerDependencies": { + "marked": "^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "marked": { "optional": true } + } +} +``` + +- [ ] **Step 5: Update cockpit example styles.css to add @source for chat library** + +In each cockpit Angular example's `src/styles.css`, add after the `@import "tailwindcss"` line: + +```css +@source "../../../../../libs/chat/src/"; +``` + +This tells Tailwind v4 to scan the chat library source for utility classes. Run this for all 14 examples: + +```bash +for dir in cockpit/langgraph/*/angular/src cockpit/deep-agents/*/angular/src; do + if [ -f "$dir/styles.css" ]; then + # Add @source line after @import "tailwindcss" if not already present + grep -q '@source.*libs/chat' "$dir/styles.css" || \ + sed -i '' 's|@import "tailwindcss";|@import "tailwindcss";\n@source "../../../../../libs/chat/src/";|' "$dir/styles.css" + fi +done +``` + +- [ ] **Step 6: Run full test suite** + +```bash +npx nx test chat +npx nx test render +npx nx test stream-resource +``` + +Expected: All tests pass across all three libraries. + +- [ ] **Step 7: Build the library** + +```bash +npx nx build chat +``` + +Expected: Build succeeds. Check `dist/libs/chat/` for compiled output. Verify the package.json in the dist includes `marked` in peerDependencies. + +- [ ] **Step 8: Build all cockpit examples** + +```bash +npx nx run-many -t build --projects='cockpit-*-angular' +``` + +Expected: All 14 examples build successfully. + +- [ ] **Step 9: Commit** + +```bash +git add -A +git commit -m "feat(chat): clean up public API, add marked peer dep, verify build" +``` + +--- + +## Summary of Issues Fixed + +| # | Severity | Issue | Fixed In | +|---|----------|-------|----------| +| 1 | BLOCKER | ChatTimelineSlider unstyled without Tailwind | Task 5 Step 4 | +| 2 | BLOCKER | ChatError mixed Tailwind/inline | Task 4 Step 3 | +| 3 | BLOCKER | Theme CSS triplicated 6x | Task 1 | +| 4 | BLOCKER | No auto-scroll | Tasks 2, 3 | +| 5 | HIGH | No textarea auto-expand | Task 4 Step 1 | +| 6 | HIGH | No markdown rendering | Tasks 1, 2, 3 | +| 7 | HIGH | Message templates duplicated | Accepted (shadcn model) | +| 8 | HIGH | focused plain boolean with OnPush | Task 4 Step 1 | +| 9 | HIGH | No empty/welcome state | Task 2 | +| 10 | HIGH | Thread sidebar not responsive | Task 2 | +| 11 | MEDIUM | CSS var fallback inconsistency | Task 1 | +| 12 | MEDIUM | Emoji icons | Tasks 4, 5 | +| 13 | MEDIUM | No selector prefix | Accepted (design decision) | +| 14 | MEDIUM | Missing ARIA | Tasks 2, 3, 4 | +| 15 | MEDIUM | Legacy export | Task 6 | +| 16 | MEDIUM | provideChat no-op | Task 6 | +| 17 | LOW | Missing debug components | Out of scope (Tier 2) | +| 18 | LOW | No keyboard navigation | Out of scope (v2) | +| 19 | LOW | getMessageType fallthrough | Accepted (safe default) | + +**Notes on accepted items:** +- **#7 (template duplication):** The shadcn model intentionally co-locates templates. Extracting them into a shared component would add coupling and make copy-paste customization harder. This is a design choice, not a bug. +- **#13 (selector prefix):** `chat` was an explicit design decision. Changing it is a breaking change. +- **#19 (getMessageType fallthrough):** Defaulting unknown types to 'ai' rendering is the safest UX choice β€” an unrecognized message type still renders rather than disappearing. diff --git a/libs/chat/package.json b/libs/chat/package.json index ad5d27e26..3e6746359 100644 --- a/libs/chat/package.json +++ b/libs/chat/package.json @@ -9,7 +9,13 @@ "@cacheplane/stream-resource": "^0.0.1", "@json-render/core": "^0.16.0", "@langchain/core": "^1.1.33", - "@langchain/langgraph-sdk": "^1.7.4" + "@langchain/langgraph-sdk": "^1.7.4", + "marked": "^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "marked": { + "optional": true + } }, "license": "PolyForm-Noncommercial-1.0.0", "sideEffects": false diff --git a/libs/chat/src/index.ts b/libs/chat/src/index.ts index 9f243f5ce..7e1a213e3 100644 --- a/libs/chat/src/index.ts +++ b/libs/chat/src/index.ts @@ -1,4 +1 @@ -export { ChatComponent } from './lib/chat.component'; -export { ChatMessageComponent } from './lib/chat-message.component'; -export { ChatInputComponent } from './lib/chat-input.component'; -export type { ChatMessage } from './lib/chat.types'; +export * from './public-api'; diff --git a/libs/chat/src/lib/chat-input.component.ts b/libs/chat/src/lib/chat-input.component.ts deleted file mode 100644 index c66a83a54..000000000 --- a/libs/chat/src/lib/chat-input.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -/** - * Chat input bar with text field and send button. - * Emits sendMessage when user submits. - */ -@Component({ - selector: 'cp-chat-input', - standalone: true, - imports: [FormsModule], - template: ` -
- - -
- `, - styles: [` - .cp-input { display: flex; gap: 0.5rem; } - .cp-input input { flex: 1; padding: 0.75rem 1rem; border: 1px solid rgba(0,64,144,0.15); border-radius: 0.5rem; background: rgba(255,255,255,0.7); color: #1a1a2e; font: inherit; font-size: 0.9rem; } - .cp-input input:focus { outline: none; border-color: rgba(0,64,144,0.3); } - .cp-input button { padding: 0.75rem 1.5rem; border: none; border-radius: 0.5rem; background: #004090; color: #fff; font: inherit; font-size: 0.85rem; cursor: pointer; transition: opacity 0.15s; } - .cp-input button:disabled { opacity: 0.5; cursor: not-allowed; } - .cp-input button:hover:not(:disabled) { opacity: 0.9; } - `], -}) -export class ChatInputComponent { - @Input() placeholder = 'Type a message...'; - @Input() disabled = false; - @Output() sendMessage = new EventEmitter(); - - text = ''; - - onSend(): void { - const msg = this.text.trim(); - if (!msg || this.disabled) return; - this.sendMessage.emit(msg); - this.text = ''; - } -} diff --git a/libs/chat/src/lib/chat-message.component.ts b/libs/chat/src/lib/chat-message.component.ts deleted file mode 100644 index da02f6b17..000000000 --- a/libs/chat/src/lib/chat-message.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component, Input } from '@angular/core'; - -/** - * Renders a single chat message bubble. - * Human messages align right with accent tint. - * AI messages align left with subtle background. - */ -@Component({ - selector: 'cp-chat-message', - standalone: true, - template: ` -
- {{ type }} -

{{ content }}

-
- `, - styles: [` - .cp-message { padding: 0.75rem 1rem; border-radius: 0.5rem; max-width: 80%; } - .cp-message--human { background: rgba(0, 64, 144, 0.08); align-self: flex-end; } - .cp-message--ai { background: rgba(0, 0, 0, 0.03); align-self: flex-start; } - .cp-message__role { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.4; display: block; margin-bottom: 2px; } - .cp-message__content { margin: 0; white-space: pre-wrap; font-size: 0.9rem; line-height: 1.6; } - `], -}) -export class ChatMessageComponent { - @Input({ required: true }) type!: 'human' | 'ai'; - @Input({ required: true }) content!: string; -} diff --git a/libs/chat/src/lib/chat.component.ts b/libs/chat/src/lib/chat.component.ts deleted file mode 100644 index 48bbcd054..000000000 --- a/libs/chat/src/lib/chat.component.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { Component, Input, Output, EventEmitter, ContentChild, TemplateRef } from '@angular/core'; -import { NgTemplateOutlet } from '@angular/common'; -import { ChatMessageComponent } from './chat-message.component'; -import { ChatInputComponent } from './chat-input.component'; - -/** - * Headful chat component for stream-resource demos. - * - * Renders a message list, input bar, and optional sidebar via content projection. - * Used by all LangGraph cockpit examples. - * - * @example - * ```html - * - * - *

Custom sidebar content

- *
- *
- * ``` - */ -@Component({ - selector: 'cp-chat', - standalone: true, - imports: [ChatMessageComponent, ChatInputComponent, NgTemplateOutlet], - template: ` -
-
-
- @for (msg of messages; track $index) { - - } - @empty { -

Send a message to start.

- } -
- @if (error) { -

{{ error }}

- } - -
- @if (sidebarTemplate) { - - } -
- `, - styles: [` - :host { display: block; height: 100%; } - .cp-chat { display: grid; grid-template-columns: 1fr; height: 100%; } - .cp-chat--with-sidebar { grid-template-columns: 1fr 260px; } - .cp-chat__main { display: flex; flex-direction: column; gap: 0.75rem; max-width: 640px; width: 100%; margin: 0 auto; padding: 1rem; height: 100%; min-height: 0; } - .cp-chat__messages { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem; } - .cp-chat__empty { color: #8b8fa3; font-size: 0.85rem; text-align: center; margin-top: 2rem; } - .cp-chat__error { color: #ef4444; font-size: 0.85rem; padding: 0.5rem; background: rgba(239,68,68,0.06); border-radius: 0.25rem; } - .cp-chat__sidebar { padding: 1rem; border-left: 1px solid rgba(0,64,144,0.08); overflow-y: auto; font-size: 0.85rem; } - @media (max-width: 768px) { - .cp-chat--with-sidebar { grid-template-columns: 1fr; } - .cp-chat__sidebar { border-left: none; border-top: 1px solid rgba(0,64,144,0.08); max-height: 200px; } - } - `], -}) -export class ChatComponent { - @Input() messages: Array<{ type: string; content: string }> = []; - @Input() isLoading = false; - @Input() error: unknown = null; - @Output() sendMessage = new EventEmitter(); - @ContentChild('sidebar') sidebarTemplate?: TemplateRef; -} diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index df44d5719..11a126a27 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -2,10 +2,16 @@ import { Component, computed, + effect, input, + inject, signal, + viewChild, + ElementRef, ChangeDetectionStrategy, + ViewEncapsulation, } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; import type { StreamResourceRef } from '@cacheplane/stream-resource'; import { ChatMessagesComponent } from '../../primitives/chat-messages/chat-messages.component'; import { MessageTemplateDirective } from '../../primitives/chat-messages/message-template.directive'; @@ -19,6 +25,8 @@ import { DebugSummaryComponent } from './debug-summary.component'; import type { DebugCheckpoint } from './debug-checkpoint-card.component'; import { toDebugCheckpoint, extractStateValues } from './debug-utils'; import { messageContent } from '../shared/message-utils'; +import { CHAT_THEME_STYLES } from '../../styles/chat-theme'; +import { CHAT_MARKDOWN_STYLES, renderMarkdown } from '../../styles/chat-markdown'; @Component({ selector: 'chat-debug', @@ -35,122 +43,61 @@ import { messageContent } from '../shared/message-utils'; DebugSummaryComponent, ], changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - `:host { - --chat-bg: #171717; --chat-bg-alt: #222222; --chat-bg-hover: #2a2a2a; - --chat-text: #e0e0e0; --chat-text-muted: #777777; --chat-text-placeholder: #666666; - --chat-border: #333333; --chat-border-light: #2a2a2a; - --chat-user-bg: #2a2a2a; --chat-user-text: #f5f5f5; --chat-user-border: #333333; - --chat-avatar-bg: #333333; --chat-avatar-text: #aaaaaa; - --chat-input-bg: #222222; --chat-input-border: #333333; --chat-input-focus-border: #555555; - --chat-send-bg: #444444; --chat-send-text: #aaaaaa; - --chat-error-bg: #2d1515; --chat-error-text: #f87171; - --chat-warning-bg: #2d2315; --chat-warning-text: #fbbf24; --chat-success: #4ade80; - --chat-radius-message: 20px; --chat-radius-input: 24px; --chat-radius-card: 12px; - --chat-radius-avatar: 8px; --chat-max-width: 720px; - --chat-font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif; - --chat-font-size: 15px; --chat-line-height: 1.6; - font-family: var(--chat-font-family); font-size: var(--chat-font-size); - line-height: var(--chat-line-height); color: var(--chat-text); background: var(--chat-bg); - display: flex; flex-direction: column; height: 100%; overflow: hidden; - } - @media (prefers-color-scheme: light) { - :host:not([data-chat-theme="dark"]) { - --chat-bg: #ffffff; --chat-bg-alt: #f5f5f5; --chat-bg-hover: #ebebeb; - --chat-text: #1a1a1a; --chat-text-muted: #999999; --chat-text-placeholder: #999999; - --chat-border: #e5e5e5; --chat-border-light: #f0f0f0; - --chat-user-bg: #f0f0f0; --chat-user-text: #1a1a1a; --chat-user-border: transparent; - --chat-avatar-bg: #f0f0f0; --chat-avatar-text: #666666; - --chat-input-bg: #f5f5f5; --chat-input-border: #e5e5e5; --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; --chat-send-text: #999999; - --chat-error-bg: #fef2f2; --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; --chat-warning-text: #d97706; --chat-success: #16a34a; - } - } - :host([data-chat-theme="light"]) { - --chat-bg: #ffffff; --chat-bg-alt: #f5f5f5; --chat-bg-hover: #ebebeb; - --chat-text: #1a1a1a; --chat-text-muted: #999999; --chat-text-placeholder: #999999; - --chat-border: #e5e5e5; --chat-border-light: #f0f0f0; - --chat-user-bg: #f0f0f0; --chat-user-text: #1a1a1a; --chat-user-border: transparent; - --chat-avatar-bg: #f0f0f0; --chat-avatar-text: #666666; - --chat-input-bg: #f5f5f5; --chat-input-border: #e5e5e5; --chat-input-focus-border: #cccccc; - --chat-send-bg: #e5e5e5; --chat-send-text: #999999; - --chat-error-bg: #fef2f2; --chat-error-text: #dc2626; - --chat-warning-bg: #fffbeb; --chat-warning-text: #d97706; --chat-success: #16a34a; - }`, - ], + encapsulation: ViewEncapsulation.None, + styles: [CHAT_THEME_STYLES, CHAT_MARKDOWN_STYLES], template: ` -
+
-
-
-
+
+
+
-
-
{{ messageContent(message) }}
+
+
{{ messageContent(message) }}
- + -
-
-
A
- Assistant +
+
+
A
+ Assistant
-
{{ messageContent(message) }}
+
-
{{ messageContent(message) }}
+
{{ messageContent(message) }}
-
- +
+ {{ messageContent(message) }}
@@ -164,8 +111,8 @@ import { messageContent } from '../shared/message-utils'; -
-
+
+
@if (!debugOpen()) {
-
+
-
+
-
+
@if (selectedCheckpointIndex() >= 0) { -
+
>(); readonly debugOpen = signal(true); @@ -265,6 +219,27 @@ export class ChatDebugComponent { // Message templates are intentionally co-located (shadcn copy-paste model) readonly messageContent = messageContent; + private readonly scrollContainer = viewChild>('scrollContainer'); + + /** Track message count to trigger auto-scroll */ + private readonly messageCount = computed(() => this.ref().messages().length); + + constructor() { + // Auto-scroll to bottom when new messages arrive or loading state changes + effect(() => { + this.messageCount(); // track + this.ref().isLoading(); // track + const el = this.scrollContainer()?.nativeElement; + if (el) { + setTimeout(() => el.scrollTop = el.scrollHeight, 0); + } + }); + } + + renderMd(content: string) { + return renderMarkdown(content, this.sanitizer); + } + stepForward(): void { const idx = this.selectedCheckpointIndex(); if (idx < this.checkpoints().length - 1) { diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts index 8e142e538..1d341b71c 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/debug-checkpoint-card.component.ts @@ -20,20 +20,20 @@ export interface DebugCheckpoint { template: `
diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts index 59a48cf35..164f48771 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/debug-state-diff.component.ts @@ -16,7 +16,7 @@ import type { DiffEntry } from './state-diff'; changeDetection: ChangeDetectionStrategy.OnPush, template: ` @if (diffEntries().length === 0) { -

No changes

+

No changes

} @else {
@for (entry of diffEntries(); track entry.path) { @@ -26,7 +26,7 @@ import type { DiffEntry } from './state-diff'; > {{ prefix(entry.type) }} {{ entry.path }} @if (entry.type === 'changed') { - {{ entry.before | json }} → {{ entry.after | json }} + {{ entry.before | json }} → {{ entry.after | json }} } @else if (entry.type === 'added') { {{ entry.after | json }} } @else { @@ -56,9 +56,9 @@ export class DebugStateDiffComponent { colorClass(type: DiffEntry['type']): string { switch (type) { - case 'added': return 'bg-green-50 text-green-700'; - case 'removed': return 'bg-red-50 text-red-700'; - case 'changed': return 'bg-amber-50 text-amber-700'; + case 'added': return 'bg-[var(--chat-bg-alt)] text-[var(--chat-success)]'; + case 'removed': return 'bg-[var(--chat-error-bg)] text-[var(--chat-error-text)]'; + case 'changed': return 'bg-[var(--chat-warning-bg)] text-[var(--chat-warning-text)]'; } } } diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts index 71000e162..0e1fd11bc 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/debug-state-inspector.component.ts @@ -13,7 +13,7 @@ import { JsonPipe } from '@angular/common'; changeDetection: ChangeDetectionStrategy.OnPush, template: `
-
{{ state() | json }}
+
{{ state() | json }}
`, }) diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts index 37b0eaf2c..16ab38ae5 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/debug-summary.component.ts @@ -13,7 +13,7 @@ import type { DebugCheckpoint } from './debug-checkpoint-card.component'; standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` -
+
{{ checkpoints().length }} step(s) {{ totalDuration() }}ms total
diff --git a/libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts b/libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts index 3f33dffe8..bdc2dda7a 100644 --- a/libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/debug-timeline.component.ts @@ -16,14 +16,14 @@ import type { DebugCheckpoint } from './debug-checkpoint-card.component'; template: `
-
+
@for (cp of checkpoints(); track $index; let i = $index) {
+