DEV Community

Cover image for How to Build an AI-Powered Writing Assistant in Angular Using Google Gemini API (Part 1)
Phinter Atieno for Syncfusion, Inc.

Posted on • Originally published at syncfusion.com on

How to Build an AI-Powered Writing Assistant in Angular Using Google Gemini API (Part 1)

TL;DR: Can your Angular app write smarter? With Google Gemini API, it’s possible! In Part 1, we set up the project, secured the API key, built the AISuggestionService for real-time grammar checks, and added a signal-based Settings Component for key and token management. Next, we’ll bring this foundation to life with an interactive UI and advanced features in Part 2.

Writing great content is challenging, correcting grammar, improving flow, and staying creative takes time. Imagine an AI assistant that checks grammar in real time, suggests improvements, and boosts your writing confidence.

In this tutorial, we’ll show you how to integrate Google’s Gemini API into an Angular app to build your own AI-powered writing assistant.

AI-powered writing assistant- What we’ll build in this series

The AI-powered writing assistant will provide:

  • A clean writing environment
  • Real-time grammar checking
  • Interactive suggestions with one-click fixes
  • Token usage tracking
  • Configurable AI settings
  • Comprehensive error handling

Steps involved

  • Setting up your Angular project
  • Obtaining your Gemini API key
  • Building the AI Suggestion service
  • Building the Settings component
  • Building the Suggestions Panel component
  • Building the Editor component

By the end of Part 1, you’ll have a working AI Suggestion Service and Settings Component ready for production.

Let’s get started!

Prerequisites

Before you start, make sure you have:

Step 1: Setting up your Angular project

First, open your terminal and create a new Angular project:

ng new angular-gemini-writing-assistant --style=scss --zoneless=false
Enter fullscreen mode Exit fullscreen mode

This command creates a new Angular workspace and initial application with the following configurations:

  • –style=scss: Sets SCSS as the stylesheet format for components (instead of CSS, LESS, or SASS).
  • –zoneless=false: Enables Zone.js for automatic change detection.

Step 2: Obtaining your Gemini API key

To integrate the Google Gemini API into your Angular app, you’ll need an API key. Follow these steps:

  1. Navigate to Google AI Studio.
  2. Sign in with your Google account.
  3. Accept the terms of service if prompted.
  4. Click Create API key.
  5. Now, a modal pop-up will open. Provide a name for your key and select a Google Cloud Project from the dropdown.
  6. Click Create key.
  7. Finally, copy your API key and store it securely.

Important: Each Gemini API key is linked to a Google Cloud project. If you don’t have a project yet, create one or import an existing project from Google Cloud into AI Studio.

Security best practice

  • Never commit your API key to version control.
  • Do not expose it in client-side code for production apps.

Next, we’ll configure our Angular app to use this API key and start building the AI Suggestion Service.

Step 3: Building the AI suggestion service — The heart of the app

The AISuggestionService is the core engine that powers your grammar assistant. It handles:

  • Communication with Google Gemini API.
  • Processing grammar suggestions.
  • Managing app state with Angular Signals.

Since this service has multiple responsibilities, we’ll break it down into smaller chunks and explain each function in detail.

1. Define the JSON Schema for structured responses

At the top of the src/app/services/ai-suggestion.ts file, define a JSON schema to enforce structured responses from Gemini.

const SUGGESTIONS_SCHEMA: JSONSchema = {
  type: "object",
  properties: {
    suggestions: {
      type: "array",
      items: {
        type: "object",
        properties: {
          text: { type: "string" },
          originalText: { type: "string" },
        },
        required: ["text"],
      },
    },
  },
  required: ["suggestions"],
};
Enter fullscreen mode Exit fullscreen mode

Why use a JSON schema?

The schema ensures a consistent structure, rather than relying on Gemini to return well-formatted JSON. This prevents complex parsing and ensures predictable, reliable responses.

2. Create the service with dependency injection

Now, create the AISuggestionService class with all necessary dependencies as mentioned in the following code example.

export class AISuggestionService {
  private readonly http = inject(HttpClient);
  private readonly modelId = inject(MODEL_ID);
  private readonly apiBase = inject(API_BASE);
  private readonly timeoutMs = inject(TIMEOUT_MS);
  private readonly maxOutputTokens = inject(MAX_OUTPUT_TOKENS);

  private get apiUrl() {
    const path = `models/${this.modelId}:generateContent`;
    return `${this.apiBase}/${path}`;
  }

  private readonly defaultApiKey = "";
  private apiKey = this.defaultApiKey;

  private readonly tokenUsage = signal({
    inputTokens: 0,
    outputTokens: 0,
    totalTokens: 0,
    requestCount: 0,
  });
  getTokenUsage = this.tokenUsage.asReadonly();

  private readonly aiStatus = signal({ kind: "ok" });
  getAIStatus = this.aiStatus.asReadonly();
}
Enter fullscreen mode Exit fullscreen mode

This service uses Angular’s modern inject() function for dependency injection and signals for reactive state management. The readonly public signals (getTokenUsage, getAIStatus) ensure components can observe but not modify the service’s internal state.

3. Add the main method: getSuggestions()

The getSuggestions() is the primary method that orchestrates the entire grammar-checking workflow.

getSuggestions(text: string): Observable<AISuggestion[]> {
  if (!this.apiKey || !this.apiKey.trim()) {
    this.aiStatus.set({
      kind: "noApiKey",
      message: "No API key configured. Please add your Gemini API key in Settings.",
    });
    return of([
      {
        id: SUGGESTION_IDS.NO_API_KEY,
        text: "No API key configured. Please go to Settings and enter your Google Gemini API key to enable grammar checking.",
      },
    ]);
  }

  if (text.trim().length < 3) return of([]);
  const prompt = this.buildGrammarPrompt(text);
  const request: GeminiRequest = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: {
      ...AISuggestionService.defaultGenerationConfig(),
      maxOutputTokens: this.maxOutputTokens,
      responseMimeType: "application/json",
      responseSchema: SUGGESTIONS_SCHEMA,
    },
  };

  const headers = new HttpHeaders({ "Content-Type": "application/json" });
  const params = new HttpParams().set("key", this.apiKey);

  return this.http
    .post<GeminiResponse>(this.apiUrl, request, { headers, params })
    .pipe(
      timeout(this.timeoutMs),
      map((response) => {
        this.aiStatus.set({ kind: "ok" });
        return this.parseSuggestionsFromGemini(response);
      }),
      catchError((error) => this.handleApiError(error))
    );
}
Enter fullscreen mode Exit fullscreen mode

What does this method do?

This orchestrates the entire grammar-checking workflow:

  • Guards against missing API keys.
  • Validates input (requires 3+ characters).
  • Builds a grammar-focused prompt.
  • Enforces JSON responses with schema validation.
  • Handles timeouts.
  • Updates AI status.
  • Parses responses defensively.
  • Converts errors into user-friendly suggestions.

4. Configure prompt engineering: buildGrammarPrompt()

The buildGrammarPrompt() method crafts a carefully designed prompt that focuses Gemini’s attention strictly on grammar.

private buildGrammarPrompt(text: string): string {
  return `You are an expert grammar checker. Analyze the following text and provide ONLY grammar suggestions for grammatical errors, spelling mistakes, and punctuation issues.

Text to analyze:
---
${text}
---

Return ONLY valid JSON (no code fences, no prose, no markdown).

Guidelines:
- Focus ONLY on grammar, spelling, and punctuation errors
- Do NOT provide clarity, style, or completion suggestions
- Provide 1-5 grammar corrections maximum
- If there are no grammar errors, return an empty suggestions array
- Include the exact original text that needs to be corrected in "originalText"`;
}
Enter fullscreen mode Exit fullscreen mode

Why is this prompt effective?

  • Clear role definition: Sets the context as “You are an expert grammar checker“.
  • Explicit limitations: Tells Gemini what NOT to do (no style suggestions).
  • Output format specification: Demands pure JSON without markdown fencing.
  • Quantity control: Limits to 5 suggestions to avoid overwhelming users.
  • Context inclusion: Requires originaltext for precise replacement in the editor.

5. Parse Gemini responses: parseSuggestionsFromGemini()

The parseSuggestionsFromGemini() method safely extracts and validates grammar suggestions from Gemini’s response.

private parseSuggestionsFromGemini(response: GeminiResponse): AISuggestion[] {
  if (response.usageMetadata) {
    this.tokenUsage.update((usage) => ({
      inputTokens:
        usage.inputTokens + (response.usageMetadata?.promptTokenCount ?? 0),
      outputTokens:
        usage.outputTokens + (response.usageMetadata?.candidatesTokenCount ?? 0),
      totalTokens:
        usage.totalTokens + (response.usageMetadata?.totalTokenCount ?? 0),
      requestCount: usage.requestCount + 1,
    }));
  }

  if (!response.candidates?.length) return [];
  const rawText = response.candidates[0]?.content?.parts?.[0]?.text;
  if (!rawText) return [];

  let parsedData: unknown;
  try {
    parsedData = JSON.parse(rawText.trim());
  } catch {
    return [];
  }

  if (!this.isSuggestionsPayload(parsedData)) return [];
  return this.mapItemsToSuggestions(parsedData.suggestions);
}
Enter fullscreen mode Exit fullscreen mode

This function returns empty arrays instead of throwing errors, uses optional chaining (?.) to safely navigate nested objects, wraps JSON parsing in try-catch, and validates structure with type guards before using the data.

6. Generation configuration

Now let’s control Gemini’s response behavior:

private static defaultGenerationConfig() {
  return {
    temperature:0.3,
    topK:40,
    topP:0.95,
    thinkingConfig:{thinkingBudget:0},
  } as const;
}
Enter fullscreen mode Exit fullscreen mode

Understanding model parameters

The parameters mentioned in the above code example control how the Gemini model should generate text:

  • temperature: 0.3: Controls randomness in token selection. Lower values (0-0.5) produce more deterministic, consistent outputs. We use 0.3 for accurate, focused grammar corrections rather than creative variations.
  • topK: 40: Limits selection to the top 40 most probable tokens at each step. This provides moderate diversity while filtering out unlikely options that could introduce errors.
  • topP: 0.95: Selects tokens whose cumulative probability reaches 0.95 (nucleus sampling). This ensures high-probability tokens are included while maintaining response quality.
  • thinkingBudget: 0: Controls the amount of extended reasoning the model performs before responding. When set to 0, the model responds immediately with its initial analysis. Higher values allow the model to spend more time “thinking through” complex problems, which are useful for tasks requiring multi-step reasoning, mathematical calculations, or deep analysis. For grammar checking, we use 0 because identifying spelling and grammar errors is straightforward pattern matching that doesn’t require extended reasoning, allowing us to save tokens and deliver faster responses.

7. Mapping and limiting suggestions

Then, convert the raw API data to application format.

private mapItemsToSuggestions(items: unknown[]): AISuggestion[] {
  const maxItems = 5;
  const picked = [] as AISuggestion[];

  for (let i = 0; i < items.length && picked.length < maxItems; i++) {
    const item = items[i];
    if (!this.isParsedSuggestion(item)) continue;
    picked.push({
      id: `gemini-${picked.length + 1}`,
      text: item.text,
      originalText: typeof item.originalText === "string" ? item.originalText : undefined,
    });
  }
  return picked;
}
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Limits to 5 suggestions: Prevents UI clutter.
  • Validates each item: Skips malformed suggestions.
  • Generates unique IDs: Each suggestion gets a gemini-1, gemini-2, etc. identifier. These IDs are essential for Angular’s @for loop tracking (enables efficient DOM updates) and for identifying which suggestion to remove when users dismiss or apply them. “You’ll see this pattern in action in Step 7 when we build the Suggestions Panel template (Part 2 of the blog)”.
  • Safely extracts optional fields: The originalText might not always be present.

Step 4: Building the Settings component

The Settings component provides a user interface for managing the Gemini API key and monitoring token usage. This component uses Angular’s modern input/output signals for parent-child communication and demonstrates several important patterns.

Let’s break down this component function by function to understand how each one works.

1. Component setup and initialization

Let’s create the Settings component and update the src/app/components/settings/settings.ts file as shown in the following code example.

export class SettingsComponent implements OnInit {
  private readonly aiSuggestionService = inject(AISuggestionService);
  readonly settings = input.required<UserSettings>();
  readonly tokenUsage = input.required<TokenUsage>();
  readonly settingsChange = output<Partial<UserSettings>>();
  readonly close = output<void>();
  protected apiKeyValue = '';

  ngOnInit() {
    this.apiKeyValue = this.settings().geminiApiKey || '';
  }

  closeModal() {
    this.close.emit();
  }
}
Enter fullscreen mode Exit fullscreen mode

Key concepts:

  • required(): Angular’s signal-based inputs that must be provided by the parent component. This replaces the older @Input() decorator.
  • output(): Signal-based outputs for emitting events to the parent. Replaces Output() with EventEmitter.
  • ngOnInit(): Initializes the component by loading the API key from parent settings into the local apiKeyValue
  • closeModal(): Emits an event to tell the parent component to hide the settings modal.

2. API key management

Let’s perform API key management with the following code.

saveApiKey(apiKey: string) {
  const trimmedKey = apiKey.trim();

  if (!this.isValidApiKey(trimmedKey)) {
    return;
  }

  this.updateApiKey(trimmedKey);
  this.closeModal();
}

private isValidApiKey(apiKey: string | undefined): apiKey is string {
  return !!apiKey && apiKey.length > 0;
}

private updateApiKey(apiKey: string | undefined): void {
  this.settingsChange.emit({ geminiApiKey: apiKey });
  this.aiSuggestionService.setApiKey(apiKey || '');
}

clearApiKey() {
  this.apiKeyValue = '';
  this.updateApiKey(undefined);
}
Enter fullscreen mode Exit fullscreen mode

How it works:

  • saveApiKey(): Trims whitespace, validates the key, updates it if valid, and closes the modal.
  • isValidApiKey(): A type guard that checks if the key is non-empty and narrows the TypeScript type from string | undefined to string.
  • updateApiKey(): Uses a dual update strategy, emits to the parent (for persistence like localStorage) and updates the service (for immediate use in API calls).
  • clearApiKey(): Clears both the input field and sends undefined to remove the API key from parent and service.

3. Template setup

Now, update the src/app/components/settings/settings.html file as per the source code from GitHub.

The Settings component highlights several modern Angular features and best practices. It uses:

  • Two-way binding with [(ngModel)] to keep the input field synchronized with the component property,
  • keydown.enter adds support for saving input using the Enter key.
  • Conditional rendering with @if ensures that usage statistics are displayed only when relevant data is available,
  • The button is automatically disabled when the input is empty or contains only whitespace.

Overall, the Settings component demonstrates clean and efficient Angular patterns, including signal-based inputs and outputs, type guards, and a clear separation between presentation and business logic.

To be continued

Thanks for reading! In this first part of our series, we laid the foundation for an AI-powered writing assistant in Angular by:

  • Setting up a new Angular project with modern configurations.
  • Obtaining and securely managing your Google Gemini API key.
  • Building the AISuggestionService, the core engine that communicates with Gemini, validates responses, and delivers structured grammar suggestions.
  • Creating the Settings Component to manage API keys and monitor token usage using Angular’s signal-based inputs and outputs.

With these pieces in place, your app now has a solid backend and configuration layer ready for real-time AI interactions.

You can also contact us through our support portal for queries. We are always happy to assist you!

AI Writing Assistant part 2 stay tuned


Related Blogs

This article was originally published at Syncfusion.com.

Top comments (1)

Collapse
 
hashbyt profile image
Hashbyt

This is an impressive start to building an AI-powered writing assistant! Your detailed breakdown of the setup process is incredibly helpful. By the way, we've also written a blog on Angular migration that you might find useful—feel free to check it out!