Skip to content
Open
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
3 changes: 2 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"json-source-map",
"natural-compare-lite",
"ajv",
"jmespath"
"jmespath",
"prismjs"
]
},
"configurations": {
Expand Down
4,526 changes: 1,594 additions & 2,932 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
},
"private": true,
"dependencies": {
"@a2ui/angular": "^0.8.3",
"@a2ui/angular": "^0.10.0",
"@a2ui/web_core": "^0.10.0",
"@angular/animations": "^21.0.0",
"@angular/cdk": "^21.0.0",
"@angular/common": "^21.0.0",
Expand All @@ -31,6 +32,7 @@
"mermaid": "^11.14.0",
"ngx-markdown": "^21.0.1",
"ngx-vflow": "^1.16.4",
"prismjs": "^1.30.0",
"rxjs": "~7.8.0",
"safevalues": "^1.2.0",
"string-to-color": "2.2.2",
Expand Down
16 changes: 8 additions & 8 deletions src/app/components/a2ui-canvas/a2ui-canvas.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
*/


import {MessageProcessor} from '@a2ui/angular';
import {Types} from '@a2ui/lit/0.8';
import {MessageProcessor} from '@a2ui/angular/v0_8';
import type {ServerToClientMessage, Surface} from '@a2ui/web_core/v0_8';
import {SimpleChanges} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
// 1p-ONLY-IMPORTS: import {beforeEach, describe, expect, it}
Expand Down Expand Up @@ -62,7 +62,7 @@ describe('A2uiCanvasComponent', () => {
root: 'root-column',
styles: {primaryColor: '#00BFFF', font: 'Arial'}
}
} as unknown as Types.ServerToClientMessage;
} as unknown as ServerToClientMessage;
component.beginRendering = message;

const changes: SimpleChanges = {
Expand Down Expand Up @@ -126,7 +126,7 @@ describe('A2uiCanvasComponent', () => {
{id: 'item-value', component: {Text: {text: {path: 'value'}}}}
]
}
} as unknown as Types.ServerToClientMessage;
} as unknown as ServerToClientMessage;
component.surfaceUpdate = message;

const changes: SimpleChanges = {
Expand Down Expand Up @@ -162,7 +162,7 @@ describe('A2uiCanvasComponent', () => {
{key: 'chart.items[4].value', valueNumber: 6}
]
}
} as unknown as Types.ServerToClientMessage;
} as unknown as ServerToClientMessage;
component.dataModelUpdate = message;

const changes: SimpleChanges = {
Expand All @@ -181,13 +181,13 @@ describe('A2uiCanvasComponent', () => {

it('should update activeSurface when surfaceId matches', () => {
const surfaceId = 'sales_data_yearly_surface';
const mockSurface = {} as Types.Surface;
const surfaces = new Map<string, Types.Surface>([[surfaceId, mockSurface]]);
const mockSurface = {} as Surface;
const surfaces = new Map<string, Surface>([[surfaceId, mockSurface]]);
mockMessageProcessor.getSurfaces.and.returnValue(surfaces);

const message = {
beginRendering: {surfaceId: surfaceId, root: 'root-column'}
} as unknown as Types.ServerToClientMessage;
} as unknown as ServerToClientMessage;

component.beginRendering = message;
component.ngOnChanges({
Expand Down
12 changes: 6 additions & 6 deletions src/app/components/a2ui-canvas/a2ui-canvas.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
* limitations under the License.
*/

import {MessageProcessor, Surface} from '@a2ui/angular';
import {Types} from '@a2ui/lit/0.8';
import {MessageProcessor, Surface} from '@a2ui/angular/v0_8';
import type { ServerToClientMessage } from "@a2ui/web_core/v0_8";
import {CommonModule} from '@angular/common';
import {ChangeDetectionStrategy, Component, computed, inject, Input, OnChanges, signal, SimpleChanges,} from '@angular/core';

Expand All @@ -37,9 +37,9 @@ import {ChangeDetectionStrategy, Component, computed, inject, Input, OnChanges,
export class A2uiCanvasComponent implements OnChanges {
private readonly processor = inject(MessageProcessor);

@Input() beginRendering: Types.ServerToClientMessage | null = null;
@Input() surfaceUpdate: Types.ServerToClientMessage | null = null;
@Input() dataModelUpdate: Types.ServerToClientMessage | null = null;
@Input() beginRendering: ServerToClientMessage | null = null;
@Input() surfaceUpdate: ServerToClientMessage | null = null;
@Input() dataModelUpdate: ServerToClientMessage | null = null;

readonly surfaceId = signal<string | null>(null);

Expand All @@ -49,7 +49,7 @@ export class A2uiCanvasComponent implements OnChanges {
constructor() {}

ngOnChanges(changes: SimpleChanges): void {
const messages: Types.ServerToClientMessage[] = [];
const messages: ServerToClientMessage[] = [];
let detectedSurfaceId: string | null = null;

if (changes['beginRendering'] && this.beginRendering && Object.keys(this.beginRendering).length > 0) {
Expand Down
105 changes: 105 additions & 0 deletions src/app/components/a2ui-canvas/a2ui-canvas.markdown.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


import {Catalog, DEFAULT_CATALOG, MessageProcessor, Theme} from '@a2ui/angular/v0_8';
import type {ServerToClientMessage} from '@a2ui/web_core/v0_8';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {provideMarkdown} from 'ngx-markdown';
// 1p-ONLY-IMPORTS: import {beforeEach, describe, expect, it}

import {A2UI_THEME} from '../../core/constants/a2ui-theme';
import {initTestBed} from '../../testing/utils';

import {A2uiCanvasComponent} from './a2ui-canvas.component';
import {provideA2uiMarkdown} from './a2ui-canvas.markdown';

describe('A2uiCanvas Markdown Rendering', () => {
let component: A2uiCanvasComponent;
let fixture: ComponentFixture<A2uiCanvasComponent>;

beforeEach(async () => {
initTestBed();

await TestBed
.configureTestingModule({
imports: [A2uiCanvasComponent],
providers: [
MessageProcessor,
{provide: Catalog, useValue: DEFAULT_CATALOG},
{provide: Theme, useValue: A2UI_THEME},
// This is the gist of the markdown integration!
provideMarkdown(),
provideA2uiMarkdown(),
],
})
.compileComponents();

fixture = TestBed.createComponent(A2uiCanvasComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should render markdown "### Something" as an <h3> element', async () => {
const surfaceId = 'markdown-heading-surface';
const beginRendering = {
beginRendering: {surfaceId, root: 'heading-comp'},
} as unknown as ServerToClientMessage;

const surfaceUpdate = {
surfaceUpdate: {
surfaceId,
components: [
{
id: 'heading-comp',
component: {
Text: {
text: {literalString: '### Something'},
},
},
},
],
},
} as unknown as ServerToClientMessage;

component.beginRendering = beginRendering;
component.surfaceUpdate = surfaceUpdate;

component.ngOnChanges({
beginRendering: {
currentValue: beginRendering,
previousValue: null,
firstChange: true,
isFirstChange: () => true,
},
surfaceUpdate: {
currentValue: surfaceUpdate,
previousValue: null,
firstChange: true,
isFirstChange: () => true,
},
});

fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();

const h3Element = fixture.nativeElement.querySelector('h3');
expect(h3Element).toBeTruthy();
expect(h3Element?.textContent?.trim()).toBe('Something');
});
});
35 changes: 35 additions & 0 deletions src/app/components/a2ui-canvas/a2ui-canvas.markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @license
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {MarkdownRenderer} from '@a2ui/angular/v0_8';
import {Provider} from '@angular/core';
import {MarkdownService} from 'ngx-markdown';

/**
* Provider that connects ngx-markdown's MarkdownService to A2UI's MarkdownRenderer.
*/
export function provideA2uiMarkdown(): Provider {
return {
provide: MarkdownRenderer,
useFactory: (markdownService: MarkdownService) => ({
render: async (markdown: string) => {
return (await markdownService.parse(markdown)) ?? '';
},
}),
deps: [MarkdownService],
};
}
44 changes: 39 additions & 5 deletions src/app/components/chat/chat.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
*/

import {Location} from '@angular/common';
import {Catalog} from '@a2ui/angular/v0_8';
import {HttpErrorResponse} from '@angular/common/http';
import {ChangeDetectionStrategy, Component, ElementRef, ErrorHandler} from '@angular/core';
import {ChangeDetectionStrategy, Component, ElementRef, ErrorHandler, Input} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MatDialog, MatDialogModule} from '@angular/material/dialog';
import { SnackbarService } from '../../core/services/snackbar.service';
Expand Down Expand Up @@ -112,6 +113,22 @@ class MockEvalTabComponent {
class TestHostComponent {
}

@Component({
changeDetection: ChangeDetectionStrategy.Default,
selector: 'testing-component',
template: '',
standalone: true,
})
class TestingComponent {
@Input() surfaceId?: string;
@Input() component?: any;
@Input() weight?: number;
}

const MOCK_A2UI_CATALOG: Catalog = {
TestingComponent: () => TestingComponent as any,
};

const SESSION_1_ID = 'session-1';
const SESSION_2_ID = 'session-2';
const TEST_APP_1_NAME = 'test-app';
Expand Down Expand Up @@ -266,6 +283,7 @@ describe('ChatComponent', () => {
TestHostComponent,
],
providers: [
{provide: Catalog, useValue: MOCK_A2UI_CATALOG},
{provide: EVAL_TAB_COMPONENT, useValue: EvalTabComponent},
{provide: SESSION_SERVICE, useValue: mockSessionService},
{provide: ARTIFACT_SERVICE, useValue: mockArtifactService},
Expand Down Expand Up @@ -534,8 +552,16 @@ describe('ChatComponent', () => {
content: {
role: 'bot',
parts: [
createA2uiPart({beginRendering: {id: '1'}}),
createA2uiPart({surfaceUpdate: {components: []}})
createA2uiPart({beginRendering: {surfaceId: 'surface-1', root: 'root-1'}}),
createA2uiPart({
surfaceUpdate: {
surfaceId: 'surface-1',
components: [{
id: 'root-1',
component: {TestingComponent: {}}
}]
}
})
]
},
};
Expand All @@ -550,8 +576,16 @@ describe('ChatComponent', () => {
const messages = component.uiEvents();
expect(component.uiEvents().length).toBe(1);
expect(component.uiEvents()[0].a2uiData).toEqual({
beginRendering: {beginRendering: {id: '1'}},
surfaceUpdate: {surfaceUpdate: {components: []}}
beginRendering: {beginRendering: {surfaceId: 'surface-1', root: 'root-1'}},
surfaceUpdate: {
surfaceUpdate: {
surfaceId: 'surface-1',
components: [{
id: 'root-1',
component: {TestingComponent: {}}
}]
}
}
});
});
});
Expand Down
30 changes: 15 additions & 15 deletions src/app/core/constants/a2ui-theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

import { Types, Styles } from '@a2ui/lit/0.8';
import { merge, type Theme } from '@a2ui/web_core/v0_8';

/** Elements */

Expand Down Expand Up @@ -160,27 +160,27 @@ const video = {
'layout-el-cv': true,
};

const aLight = Styles.merge(a, { 'color-c-p30': true });
const inputLight = Styles.merge(input, { 'color-c-n5': true });
const textareaLight = Styles.merge(textarea, { 'color-c-n5': true });
const buttonLight = Styles.merge(button, { 'color-c-n100': true });
const h1Light = Styles.merge(h1, { 'color-c-n5': true });
const h2Light = Styles.merge(h2, { 'color-c-n5': true });
const h3Light = Styles.merge(h3, { 'color-c-n5': true });
const bodyLight = Styles.merge(body, { 'color-c-n5': true });
const pLight = Styles.merge(p, { 'color-c-n60': true });
const preLight = Styles.merge(pre, { 'color-c-n35': true });
const orderedListLight = Styles.merge(orderedList, {
const aLight = merge(a, { 'color-c-p30': true });
const inputLight = merge(input, { 'color-c-n5': true });
const textareaLight = merge(textarea, { 'color-c-n5': true });
const buttonLight = merge(button, { 'color-c-n100': true });
const h1Light = merge(h1, { 'color-c-n5': true });
const h2Light = merge(h2, { 'color-c-n5': true });
const h3Light = merge(h3, { 'color-c-n5': true });
const bodyLight = merge(body, { 'color-c-n5': true });
const pLight = merge(p, { 'color-c-n60': true });
const preLight = merge(pre, { 'color-c-n35': true });
const orderedListLight = merge(orderedList, {
'color-c-n35': true,
});
const unorderedListLight = Styles.merge(unorderedList, {
const unorderedListLight = merge(unorderedList, {
'color-c-n35': true,
});
const listItemLight = Styles.merge(listItem, {
const listItemLight = merge(listItem, {
'color-c-n35': true,
});

export const A2UI_THEME: Types.Theme = {
export const A2UI_THEME: Theme = {
additionalStyles: {
Card: {
// 'min-width': '320px',
Expand Down
Loading
Loading