Skip to content

Commit 2a9421e

Browse files
committed
feat (angular): Align UI parts, tools, tests and example for v6
1 parent dbdba9b commit 2a9421e

18 files changed

+689
-2508
lines changed

.changeset/large-rocks-glow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@ai-sdk/angular': patch
3+
---
4+
5+
Align with v6

examples/angular/README.md

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
1-
# Angular AI Chat
1+
# Angular AI SDK Example
22

3-
A simple chat application built with Angular that connects to OpenAI models using the AI SDK.
3+
A small Angular app that exercises the AI SDK UI package with chat, completion, and structured object generation.
44

55
## Setup
66

77
```bash
88
# Install dependencies
99
pnpm install
1010

11-
# Create .env file with your OpenAI API key
12-
echo "OPENAI_API_KEY=your_key_here" > .env
11+
# Create .env file with your AI Gateway API key
12+
echo "AI_GATEWAY_API_KEY=your_key_here" > .env
13+
14+
# Or use OIDC authentication
15+
# echo "VERCEL_OIDC_TOKEN=your_token_here" > .env
1316

1417
# Start the app
1518
pnpm start
@@ -19,15 +22,19 @@ This runs both the Angular frontend (localhost:4200) and Express backend (localh
1922

2023
## Tech Stack
2124

22-
- Angular 19
25+
- Angular 20
2326
- Express.js backend
24-
- AI SDK (@ai-sdk/angular, @ai-sdk/openai)
25-
- OpenAI GPT models
27+
- AI SDK (@ai-sdk/angular, ai)
28+
- AI Gateway (default provider)
2629

2730
## Features
2831

2932
- Real-time chat interface
33+
- Completion + structured object examples
3034
- Message streaming
35+
- Fake weather tool (server-side)
36+
- Reasoning stream (supported models only)
3137
- Proxy configuration for API requests
3238

3339
Set your preferred model in `chat.component.ts` by changing the `selectedModel` parameter.
40+
Use AI Gateway model IDs like `openai/gpt-5.2`.

examples/angular/angular.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,12 @@
4343
"outputPath": "dist/app",
4444
"index": "src/index.html",
4545
"browser": "src/main.ts",
46-
"polyfills": ["zone.js"],
46+
"polyfills": [],
4747
"tsConfig": "tsconfig.app.json",
48+
"allowedCommonJsDependencies": [
49+
"@opentelemetry/api",
50+
"@vercel/oidc"
51+
],
4852
"assets": [
4953
{
5054
"glob": "**/*",

examples/angular/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@
2424
"ai": "6.0.77",
2525
"dotenv": "16.4.5",
2626
"express": "5.0.1",
27-
"rxjs": "~7.8.0",
2827
"tslib": "^2.3.0",
29-
"zod": "3.25.76",
30-
"zone.js": "~0.15.0"
28+
"zod": "3.25.76"
3129
},
3230
"devDependencies": {
3331
"@angular-devkit/build-angular": "^20.3.3",
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
1+
import {
2+
ApplicationConfig,
3+
provideZonelessChangeDetection,
4+
} from '@angular/core';
25
import { provideRouter } from '@angular/router';
36

47
import { routes } from './app.routes';
58

69
export const appConfig: ApplicationConfig = {
7-
providers: [
8-
provideZoneChangeDetection({ eventCoalescing: true }),
9-
provideRouter(routes),
10-
],
10+
providers: [provideZonelessChangeDetection(), provideRouter(routes)],
1111
};

examples/angular/src/app/chat/chat.component.css

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
white-space: pre-wrap;
5151
}
5252

53+
details {
54+
margin-bottom: 0.5rem;
55+
}
56+
5357
.tool-call {
5458
background-color: #e9ecef;
5559
border: 1px solid #ced4da;
@@ -93,6 +97,64 @@
9397
font-size: 0.8rem;
9498
}
9599

100+
.tool-error {
101+
margin-top: 0.5rem;
102+
color: #b91c1c;
103+
font-size: 0.85rem;
104+
}
105+
106+
.file-part,
107+
.source-part,
108+
.data-part {
109+
background-color: #ffffff;
110+
border: 1px solid #e2e8f0;
111+
border-radius: 8px;
112+
padding: 0.75rem;
113+
margin-top: 0.5rem;
114+
}
115+
116+
.file-image {
117+
max-width: 100%;
118+
border-radius: 6px;
119+
margin-bottom: 0.5rem;
120+
}
121+
122+
.file-link,
123+
.source-link {
124+
color: #0f172a;
125+
text-decoration: underline;
126+
font-weight: 500;
127+
}
128+
129+
.file-meta,
130+
.source-meta {
131+
margin-top: 0.35rem;
132+
font-size: 0.75rem;
133+
color: #64748b;
134+
}
135+
136+
.source-title,
137+
.data-title {
138+
font-weight: 600;
139+
color: #0f172a;
140+
}
141+
142+
.data-body {
143+
margin-top: 0.35rem;
144+
font-size: 0.85rem;
145+
font-family: monospace;
146+
white-space: pre-wrap;
147+
}
148+
149+
.step-separator {
150+
margin: 0.5rem 0;
151+
font-size: 0.7rem;
152+
font-weight: 600;
153+
color: #64748b;
154+
text-transform: uppercase;
155+
letter-spacing: 0.08em;
156+
}
157+
96158
.input-form {
97159
display: flex;
98160
padding: 1rem;

examples/angular/src/app/chat/chat.component.html

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,89 @@
33
@for (message of chat.messages; track message.id) {
44
<div class="message" [ngClass]="message.role">
55
@for (part of message.parts; track $index) {
6-
@switch (part.type) {
7-
@case ('text') {
8-
<div style="white-space: pre-wrap">
9-
{{ part.text }}
10-
@if (part.state === 'streaming') {
11-
<span class="typing-cursor">&#9646;</span>
12-
}
6+
@if (part.type === 'text') {
7+
<div style="white-space: pre-wrap">
8+
{{ part.text }}
9+
@if (part.state === 'streaming') {
10+
<span class="typing-cursor">&#9646;</span>
11+
}
12+
</div>
13+
} @else if (part.type === 'reasoning') {
14+
<details>
15+
<summary>Reasoning</summary>
16+
<div style="white-space: pre-wrap; font-size: 90%; opacity: 80%">
17+
<div>{{ part.text }}</div>
1318
</div>
14-
}
15-
@case ('reasoning') {
16-
<details>
17-
<summary>Reasoning</summary>
18-
<div
19-
style="white-space: pre-wrap; font-size: 90%; opacity: 80%"
19+
</details>
20+
} @else if (part.type === 'file') {
21+
<div class="file-part">
22+
@if (part.mediaType.startsWith('image/')) {
23+
<img
24+
class="file-image"
25+
[src]="part.url"
26+
[alt]="part.filename ?? 'image attachment'"
27+
/>
28+
} @else {
29+
<a
30+
class="file-link"
31+
[href]="part.url"
32+
target="_blank"
33+
rel="noreferrer"
2034
>
21-
<div>{{ part.text }}</div>
35+
{{ part.filename ?? part.url }}
36+
</a>
37+
}
38+
<div class="file-meta">{{ part.mediaType }}</div>
39+
</div>
40+
} @else if (part.type === 'source-url') {
41+
<div class="source-part">
42+
<a
43+
class="source-link"
44+
[href]="part.url"
45+
target="_blank"
46+
rel="noreferrer"
47+
>
48+
{{ part.title ?? part.url }}
49+
</a>
50+
<div class="source-meta">Source: {{ part.sourceId }}</div>
51+
</div>
52+
} @else if (part.type === 'source-document') {
53+
<div class="source-part">
54+
<div class="source-title">{{ part.title }}</div>
55+
<div class="source-meta">
56+
{{ part.filename ?? part.mediaType }}
57+
</div>
58+
</div>
59+
} @else if (part.type === 'step-start') {
60+
<!-- <div class="step-separator">Step</div> -->
61+
} @else if (isToolPart(part)) {
62+
<div class="tool-call">
63+
<div class="tool-name">
64+
Tool: {{ part.type.replace('tool-', '') }}
65+
</div>
66+
<div class="tool-status">Status: {{ part.state }}</div>
67+
@if (part.input !== undefined) {
68+
<pre class="tool-input">{{ part.input | json }}</pre>
69+
}
70+
@if (part.output !== undefined) {
71+
<div class="tool-result-container">
72+
<div class="tool-result-title">Result</div>
73+
<pre class="tool-result">{{ part.output | json }}</pre>
2274
</div>
23-
</details>
24-
}
25-
@default {
26-
<code>{{ part | json }}</code>
27-
}
75+
}
76+
@if (part.errorText) {
77+
<div class="tool-error">{{ part.errorText }}</div>
78+
}
79+
</div>
80+
} @else if (isDataPart(part)) {
81+
<div class="data-part">
82+
<div class="data-title">
83+
Data: {{ part.type.replace('data-', '') }}
84+
</div>
85+
<pre class="data-body">{{ part.data | json }}</pre>
86+
</div>
87+
} @else {
88+
<code>{{ part | json }}</code>
2889
}
2990
}
3091
</div>

examples/angular/src/app/chat/chat.component.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import {
77
Validators,
88
} from '@angular/forms';
99
import { Chat } from '@ai-sdk/angular';
10+
import {
11+
isToolUIPart,
12+
type DataUIPart,
13+
type ToolUIPart,
14+
type UIDataTypes,
15+
type UIMessagePart,
16+
type UITools,
17+
} from 'ai';
1018

1119
@Component({
1220
selector: 'app-chat',
@@ -27,6 +35,18 @@ export class ChatComponent {
2735
});
2836
}
2937

38+
isToolPart(
39+
part: UIMessagePart<UIDataTypes, UITools>,
40+
): part is ToolUIPart<UITools> {
41+
return isToolUIPart(part);
42+
}
43+
44+
isDataPart(
45+
part: UIMessagePart<UIDataTypes, UITools>,
46+
): part is DataUIPart<UIDataTypes> {
47+
return part.type.startsWith('data-');
48+
}
49+
3050
sendMessage() {
3151
if (this.chatForm.invalid) {
3252
return;
@@ -41,7 +61,7 @@ export class ChatComponent {
4161
},
4262
{
4363
body: {
44-
selectedModel: 'gpt-4.1',
64+
selectedModel: 'openai/gpt-5.2',
4565
},
4666
},
4767
);

0 commit comments

Comments
 (0)