TL;DR: In Part 2, we create the interactive UI for our AI Writing Assistant using Angular and Google Gemini. We’ll build a Suggestions Panel, a smart Editor component, and configure the template for a seamless writing experience. This architecture utilizes signals, effects, computed properties, and clean component composition to achieve scalability and performance.
In Part 1, we establish the foundation for our AI Writing Assistant by integrating the Google Gemini API, creating services, and managing settings.
Now, in Part 2, we’ll bring everything together with an interactive UI that includes:
Let’s dive in!
The Suggestions Panel is a sleek component that displays grammar suggestions and allows users to apply or dismiss them. It serves as a presentational component, receiving input data and emitting output actions without business logic.
Let’s create the suggestions panel component and update the src/app/components/suggestions-panel/suggestions-panel.ts file.
Refer to the following code.
export class SuggestionsPanelComponent {
suggestions = input.required<AISuggestion[]>();
applySuggestion = output<AISuggestion>();
dismissSuggestion = output<AISuggestion>();
} Component architecture highlights:
Now, update the src/app/components/suggestions-panel/suggestions-panel.html file as per the source code from GitHub.
Key Angular features in the template:
@for loop with track to iterate over suggestions while ensuring efficient rendering and minimal DOM updates.originalText exists, hiding it for error suggestions.applySuggestion.emit(suggestion), removing the need for extra wrapper methods and keeping the logic clean.This clear separation of concerns makes the component both highly reusable and easy to test.
The Editor component is the main container that brings together all the pieces:
This smart component manages state, handles user interactions, and orchestrates communication between child components.
Let’s break down this complex component into digestible chunks.
Create the Editor component and update the src/app/components/editor/editor.ts file as shown below.
export class EditorComponent {
wordCount = computed(() => {
const text = this.currentText().trim();
if (!text) return 0;
return text.split(/\s+/).filter((word) => word.length > 0).length;
});
characterCount = computed(() => this.currentText().length);
filteredSuggestions = computed(() => this.suggestions().slice(0, 5));
} The computed signals automatically recalculate when their dependencies change:
These computed values are reactive. When currentText() or suggestions() change, they automatically update without manual recalculation.
Refer to the following code example to add autosuggestions with debouncing.
private setupSuggestionsEffect() {
effect((onCleanup) => {
const text = this.currentText().trim();
const auto = this.settings().autoSuggestions;
if (this.debounceHandle) {
clearTimeout(this.debounceHandle);
this.debounceHandle = undefined;
}
if (!text || !auto) {
this.suggestions.set([]);
this.isProcessing.set(false);
return;
}
if (text === this.lastQuery) {
return;
}
this.debounceHandle = setTimeout(() => {
this.requestSuggestions(text);
}, 500);
onCleanup(() => {
if (this.debounceHandle) {
clearTimeout(this.debounceHandle);
this.debounceHandle = undefined;
}
});
});
} How this works:
currentText() or settings().autoSuggestions changes.This pattern prevents excessive API calls while the user is actively typing.
Then, configure the API status monitoring effect by referring to the following code example.
private setupAiStatusNoticeEffect() {
effect(() => {
const status = this.aiSuggestionService.getAIStatus();
if (status.kind === 'invalidApiKey' || status.kind === 'noApiKey') {
this.apiKeyNoticeMessage.set(
status.message || 'Your API key seems invalid. Update it in Settings.'
);
this.showApiKeyNotice.set(true);
} else {
this.showApiKeyNotice.set(false);
this.apiKeyNoticeMessage.set('');
}
});
} Purpose: Automatically displays a notice banner when there are API key issues. This effect occurs whenever the AI service’s status changes, providing users with real-time feedback.
Refer to the following code example for requesting suggestions.
private requestSuggestions(text: string): Subscription {
this.isProcessing.set(true);
return this.aiSuggestionService
.getSuggestions(text)
.pipe(
catchError(() =>
of([
{
id: SUGGESTION_IDS.API_ERROR,
text: 'Failed to get suggestions.',
},
])
),
finalize(() => this.isProcessing.set(false)),
takeUntilDestroyed(this.destroyRef)
)
.subscribe((suggestions) => {
const filtered = suggestions.filter(
(s) =>
s.id !== SUGGESTION_IDS.NO_API_KEY &&
s.id !== SUGGESTION_IDS.INVALID_API_KEY
);
this.suggestions.set(filtered);
this.lastQuery = text;
});
} RxJS highlights:
Let’s implement the code logic to apply and dismiss suggestions.
applySuggestion(suggestion: AISuggestion) {
const currentText = this.currentText();
let newText = currentText;
if (suggestion.originalText) {
newText = currentText.replace(suggestion.originalText, suggestion.text);
} else {
newText = suggestion.text;
}
this.currentText.set(newText);
this.dismissSuggestion(suggestion);
}
dismissSuggestion(suggestion: AISuggestion) {
this.suggestions.update((suggestions) =>
suggestions.filter((s) => s.id !== suggestion.id)
);
} Suggestion management:
applySuggestion: Replaces the original text with the corrected text if originalText exists, otherwise just sets the new text.dismissSuggestion: Removes the suggestion from the array using an immutable update pattern.Refer to the following example code to manage settings.
onSettingsChange(updates: Partial<UserSettings>) {
this.settings.update((current) => ({ ...current, ...updates }));
this.settingsService.updateUserSettings(updates);
if (updates.geminiApiKey !== undefined) {
this.aiSuggestionService.setApiKey(updates.geminiApiKey || '');
this.showApiKeyNotice.set(false);
this.apiKeyNoticeMessage.set('');
}
}
toggleSettings() {
this.showSettings.set(!this.showSettings());
}
closeSettings() {
this.showSettings.set(false);
}
toggleAutoSuggestions() {
const current = this.settings().autoSuggestions;
this.onSettingsChange({ autoSuggestions: !current });
if (!this.settings().autoSuggestions) {
this.suggestions.set([]);
this.isProcessing.set(false);
}
}
dismissApiKeyNotice() {
this.showApiKeyNotice.set(false);
} Settings coordination:
onSettingsChange: Updates local state, persists storage, and updates the AI service.toggleAutoSuggestions: Clears suggestions when autosuggestions are turned off.Now, update the src/app/components/editor/editor.html file as shown below.
@if (showApiKeyNotice()) {
<div class="inline-api-key-notice" role="status" aria-live="polite">
<span class="notice-icon">⚠️</span>
<span class="notice-text">
{{ apiKeyNoticeMessage() || "Your API key seems invalid. Update it in Settings." }}
</span>
<button class="notice-action" type="button" (click)="toggleSettings()">
Open Settings
</button>
<button
class="notice-dismiss"
type="button"
(click)="dismissApiKeyNotice()"
title="Dismiss"
>
X
</button>
</div>
}
@if (showSettings()) {
<app-settings
[settings]="settings()"
[tokenUsage]="tokenUsage()"
(settingsChange)="onSettingsChange($event)"
(close)="closeSettings()"
/>
}
<div class="editor-container">
<div class="editor-main">
<textarea
#textEditor
[(ngModel)]="currentText"
placeholder="Start writing your masterpiece..."
class="main-textarea"
></textarea>
@if (isProcessing()) {
<div
class="inline-processing-indicator"
aria-live="polite"
aria-busy="true"
>
<div class="loading-spinner"></div>
<span>Analyzing...</span>
</div>
}
</div>
@if (settings().autoSuggestions) {
<app-suggestions-panel
[suggestions]="filteredSuggestions()"
(applySuggestion)="applySuggestion($event)"
(dismissSuggestion)="dismissSuggestion($event)"
/>
}
</div> Template highlights:
{{ wordCount() }} and {{ characterCount() }} update reactively.<app-settings> and <app-suggestions-panel> child components.[(ngModel)]="currentText" syncs the textarea with the signal.applySuggestion and dismissSuggestion actions.This editor component demonstrates modern Angular architecture with signals, effects, computed properties, and clean component composition.
Finally, run the app using the command ng serve.
Navigate to http://localhost:4200/ and you can see the AI Writing Assistant app in action.
Refer to the following GIF image.
For more details, refer to the complete source code on GitHub, and a live demo to see it in action.
Thanks for reading! We’ve just built a production-ready AI grammar assistant that combines Angular’s modern reactive patterns with Google’s Gemini API. More importantly, we’ve learned architectural principles that extend far beyond this single app.
We’ve implemented a blueprint for building an intelligent application:
These patterns work whether you’re building chatbots, content generators, recommendation engines, or code assistants. The principles remain the same: type safety, reactive state, error resilience, and clean separation of concerns.
We now have a foundation for creating AI-powered experiences that are maintainable, testable, and delightful to use in Angular.
Syncfusion’s Angular component library offers over 145 high-performance, lightweight, and responsive UI components, including data grids, charts, calendars, and editors. Plus, an AI-powered coding assistant streamlines development and enhances efficiency.
If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.
You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!