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
6 changes: 6 additions & 0 deletions apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -5873,6 +5873,12 @@
"type": "string",
"description": "Present when role === 'tool'.",
"optional": true
},
{
"name": "toolCallIds",
"type": "string[]",
"description": "IDs of tool calls emitted BY this assistant message. Populated by\nadapters from provider-native fields (LangGraph: `tool_calls[].id`;\nAnthropic: `tool_use` content blocks). Used by `<chat-tool-calls>` to\nscope its rendering to a single message — otherwise it falls back to\nthe agent's global toolCalls list, which renders duplicates across\nevery AI message in a thread.",
"optional": true
}
],
"examples": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,94 @@ import { Component, computed, input } from '@angular/core';
selector: 'app-bar-chart',
standalone: true,
template: `
<div class="rounded-lg border border-white/10 bg-white/5 p-4 backdrop-blur-sm">
<div class="text-sm font-medium text-white/60 mb-3">{{ title() }}</div>
<div class="chart-card">
<div class="chart-card__title">{{ title() }}</div>
@if (isSkeleton()) {
<div class="skeleton skeleton-chart"></div>
} @else {
<svg [attr.viewBox]="'0 0 ' + width + ' ' + height" class="w-full" preserveAspectRatio="xMidYMid meet">
<svg
[attr.viewBox]="'0 0 ' + width + ' ' + height"
class="chart-card__svg"
preserveAspectRatio="xMidYMid meet"
role="img"
[attr.aria-label]="title()"
>
<!-- Baseline -->
<line
[attr.x1]="padding.left"
[attr.x2]="width - padding.right"
[attr.y1]="height - padding.bottom"
[attr.y2]="height - padding.bottom"
stroke="rgba(255,255,255,0.08)"
stroke-width="1"
/>
@for (bar of bars(); track $index) {
<!-- Bar -->
<rect class="bar" [attr.x]="bar.x" [attr.y]="bar.y" [attr.width]="bar.w" [attr.height]="bar.h" fill="#d4aa6a" rx="2" />
<!-- Value above bar -->
<text [attr.x]="bar.x + bar.w / 2" [attr.y]="bar.y - 6" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-size="10">{{ bar.value }}</text>
<!-- Label below bar -->
<text [attr.x]="bar.x + bar.w / 2" [attr.y]="height - 4" text-anchor="middle" fill="rgba(255,255,255,0.3)" font-size="10">{{ bar.label }}</text>
<rect
class="bar"
[attr.x]="bar.x"
[attr.y]="bar.y"
[attr.width]="bar.w"
[attr.height]="bar.h"
[attr.rx]="3"
fill="url(#bar-gradient)"
/>
<text
[attr.x]="bar.x + bar.w / 2"
[attr.y]="bar.y - 6"
text-anchor="middle"
fill="rgba(255,255,255,0.7)"
font-size="11"
font-weight="500"
>{{ bar.value }}</text>
<text
[attr.x]="bar.x + bar.w / 2"
[attr.y]="height - padding.bottom + 16"
text-anchor="middle"
fill="rgba(255,255,255,0.5)"
font-size="11"
>{{ bar.label }}</text>
}
<defs>
<linearGradient id="bar-gradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e0b87a" stop-opacity="1"/>
<stop offset="100%" stop-color="#d4aa6a" stop-opacity="0.75"/>
</linearGradient>
</defs>
</svg>
}
</div>
`,
styleUrls: ['./skeleton.css'],
styles: [`
.chart-card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(4px);
}
.chart-card__title {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.65);
letter-spacing: 0.01em;
}
.chart-card__svg {
width: 100%;
height: auto;
max-height: 280px;
display: block;
}
.bar {
transition: opacity 120ms ease;
}
.bar:hover {
opacity: 0.85;
}
`],
})
export class BarChartComponent {
readonly title = input<string>('');
Expand All @@ -32,8 +101,8 @@ export class BarChartComponent {
readonly valueKey = input<string>('');

readonly width = 400;
readonly height = 200;
readonly padding = { top: 30, right: 20, bottom: 30, left: 20 };
readonly height = 220;
readonly padding = { top: 28, right: 16, bottom: 28, left: 16 };

readonly isSkeleton = computed(() => this.data() == null);

Expand All @@ -46,7 +115,7 @@ export class BarChartComponent {
const maxVal = Math.max(...values) || 1;
const plotW = this.width - this.padding.left - this.padding.right;
const plotH = this.height - this.padding.top - this.padding.bottom;
const gap = 8;
const gap = 12;
const barW = (plotW - gap * (d.length - 1)) / d.length;

return d.map((item, i) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,19 @@ describe('ContainerComponent', () => {
fixture = TestBed.createComponent(ContainerComponent);
});

it('applies column flex classes by default', () => {
it('sets column direction attribute by default', () => {
fixture.componentRef.setInput('spec', emptySpec);
fixture.detectChanges();
const wrapper = fixture.nativeElement.querySelector('div');
expect(wrapper?.className).toContain('flex-col');
expect(wrapper?.className).not.toContain('flex-row');
const wrapper = fixture.nativeElement.querySelector('div.container');
expect(wrapper?.getAttribute('data-direction')).toBe('column');
});

it('applies row flex classes when direction is "row"', () => {
it('sets row direction attribute when direction is "row"', () => {
fixture.componentRef.setInput('spec', emptySpec);
fixture.componentRef.setInput('direction', 'row');
fixture.detectChanges();
const wrapper = fixture.nativeElement.querySelector('div');
expect(wrapper?.className).toContain('flex-row');
expect(wrapper?.className).not.toContain('flex-col');
const wrapper = fixture.nativeElement.querySelector('div.container');
expect(wrapper?.getAttribute('data-direction')).toBe('row');
});

it('renders one render-element per childKey', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,41 @@ import { RenderElementComponent } from '@ngaf/render';
standalone: true,
imports: [RenderElementComponent],
template: `
<div [class]="layoutClass()">
<div class="container" [attr.data-direction]="direction()" [style.--container-cols]="rowChildCount()">
@for (key of childKeys(); track key) {
<render-element [elementKey]="key" [spec]="spec()" />
}
</div>
`,
styles: [`
.container {
display: grid;
gap: 12px;
min-width: 0;
}
.container[data-direction="column"] {
grid-template-columns: 1fr;
}
.container[data-direction="row"] {
grid-template-columns: repeat(var(--container-cols, 1), minmax(0, 1fr));
}
/* Responsive collapse: at narrow widths, row containers stack to single column */
@media (max-width: 720px) {
.container[data-direction="row"] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 480px) {
.container[data-direction="row"] {
grid-template-columns: 1fr;
}
}
`],
})
export class ContainerComponent {
readonly childKeys = input<string[]>([]);
readonly spec = input.required<Spec>();
readonly direction = input<'row' | 'column'>('column');

readonly layoutClass = computed(() =>
this.direction() === 'row'
? 'flex flex-row flex-wrap gap-3'
: 'flex flex-col gap-3'
);
readonly rowChildCount = computed(() => Math.max(1, this.childKeys().length));
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,11 @@ describe('DashboardGridComponent', () => {
fixture = TestBed.createComponent(DashboardGridComponent);
});

it('applies vertical flex layout with section spacing', () => {
it('applies the dashboard-grid layout class', () => {
fixture.componentRef.setInput('spec', emptySpec);
fixture.detectChanges();
const wrapper = fixture.nativeElement.querySelector('div');
expect(wrapper?.className).toContain('flex-col');
expect(wrapper?.className).toContain('gap-6');
const wrapper = fixture.nativeElement.querySelector('div.dashboard-grid');
expect(wrapper).toBeTruthy();
});

it('renders one render-element per childKey', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ import { RenderElementComponent } from '@ngaf/render';
standalone: true,
imports: [RenderElementComponent],
template: `
<div class="flex flex-col gap-6 p-4">
<div class="dashboard-grid">
@for (key of childKeys(); track key) {
<render-element [elementKey]="key" [spec]="spec()" />
}
</div>
`,
styles: [`
.dashboard-grid {
display: flex;
flex-direction: column;
gap: 16px;
padding: 4px 0;
width: 100%;
min-width: 0;
}
`],
})
export class DashboardGridComponent {
readonly childKeys = input<string[]>([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,88 @@ import { Component, computed, input } from '@angular/core';
selector: 'app-data-grid',
standalone: true,
template: `
<div class="rounded-lg border border-white/10 bg-white/5 p-4 backdrop-blur-sm">
<div class="text-sm font-medium text-white/60 mb-3">{{ title() }}</div>
<div class="data-grid">
<div class="data-grid__title">{{ title() }}</div>
@if (isSkeleton()) {
@for (i of skeletonRows; track i) {
<div class="skeleton skeleton-row"></div>
}
} @else {
<table class="w-full text-sm">
<thead>
<tr class="border-b border-white/10">
@for (col of formattedColumns(); track col.key) {
<th class="text-left text-xs font-medium uppercase tracking-wider text-white/40 py-2 px-2">{{ col.label }}</th>
}
</tr>
</thead>
<tbody>
@for (row of rows(); track $index) {
<tr class="border-b border-white/5" [class.bg-white/5]="$index % 2 === 1">
<div class="data-grid__scroll">
<table class="data-grid__table">
<thead>
<tr>
@for (col of formattedColumns(); track col.key) {
<td class="py-2 px-2 text-white/80">{{ row[col.key] }}</td>
<th>{{ col.label }}</th>
}
</tr>
}
</tbody>
</table>
</thead>
<tbody>
@for (row of rows(); track $index) {
<tr>
@for (col of formattedColumns(); track col.key) {
<td>{{ row[col.key] }}</td>
}
</tr>
}
</tbody>
</table>
</div>
}
</div>
`,
styleUrls: ['./skeleton.css'],
styles: [`
.data-grid {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 18px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 10px;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(4px);
}
.data-grid__title {
font-size: 13px;
font-weight: 600;
color: rgba(255, 255, 255, 0.65);
letter-spacing: 0.01em;
}
.data-grid__scroll {
overflow-x: auto;
margin: 0 -4px;
}
.data-grid__table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
.data-grid__table th {
text-align: left;
padding: 8px 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.45);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
white-space: nowrap;
}
.data-grid__table td {
padding: 10px;
color: rgba(255, 255, 255, 0.85);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
white-space: nowrap;
}
.data-grid__table tbody tr:nth-child(even) td {
background: rgba(255, 255, 255, 0.02);
}
.data-grid__table tbody tr:last-child td {
border-bottom: none;
}
`],
})
export class DataGridComponent {
readonly title = input<string>('');
Expand Down
Loading
Loading