INTEGRATION GUIDE

Documentation for Vitto

Vitto Rive Avatar — Integration Guide

Quick Start

1. Install Rive Runtime

yarn add @rive-app/canvas

2. Place the .riv File

Once built by an animator following RIVE-PRODUCTION-SPEC.md, place the file at:

apps/gograb-ai/src/assets/rive/vitto-avatar.riv

3. Use the NX Library (Already Set Up)

The library is available at @libs-anim/vitto-rive-avatar:

libs/libs-anim/vitto-rive-avatar/
├── src/
│   ├── lib/
│   │   ├── components/
│   │   │   └── vitto-rive-avatar/
│   │   ├── services/
│   │   │   └── vitto-mood.service.ts
│   │   └── types/
│   │       └── vitto-mood.types.ts
│   └── index.ts
├── project.json
├── package.json
├── tsconfig.lib.json
└── README.md

Import path: @libs-anim/vitto-rive-avatar (already in tsconfig.base.json paths).

4. Use in Any Component

import { Component, inject } from '@angular/core';
import { VittoRiveAvatarComponent, VittoMoodService, VittoMood } from '@libs-anim/vitto-rive-avatar';

@Component({
  selector: 'ai-chat-panel',
  standalone: true,
  imports: [VittoRiveAvatarComponent],
  template: `
    <div class="chat-header">
      <ai-vitto-rive-avatar
        [size]="120"
        [enableGazeTracking]="true"
        (moodChanged)="onMoodChange($event)"
      />
      <span class="mood-label">{{ moodService.currentMoodMeta().description }}</span>
    </div>
  `,
})
export class ChatPanelComponent {
  readonly moodService = inject(VittoMoodService);

  onMoodChange(mood: VittoMood): void {
    console.log('Vitto mood changed to:', mood);
  }
}

Architecture

Signal Flow

ConversationMoodState (NGXS)
       │
       ▼
VittoMoodService.setMood('cheerful')    ←── debounced, validated
       │
       ├── _currentMood signal updates
       ├── currentMoodIndex computed updates
       │
       ▼
VittoRiveAvatarComponent (effect)
       │
       ├── Sets Rive input: moodIndex = 2
       ├── Sets Rive input: gazeX/gazeY (from mouse)
       └── Sets Rive input: isSpeaking (from AI response state)
       │
       ▼
Rive State Machine ("MoodController")
       │
       ├── Transitions to "cheerful" state
       ├── Plays processing beat (eyes → ears → details)
       └── Starts cheerful idle loop

Mood Sources

Mood can be set from multiple sources. The service handles deduplication and debouncing:

// From conversation AI analysis
this.moodService.setMood(analyzedMood);

// From manual UI control (mood picker)
this.moodService.setMoodImmediate('celebratory');

// From AI response state
this.moodService.setSpeaking(true);  // while streaming response
this.moodService.setSpeaking(false); // when done

Gaze Tracking

Mouse gaze tracking is automatically handled by the component when enableGazeTracking is true. The flow:

  1. mousemove event on document (throttled to 60fps)
  2. Mouse position normalized to -1..1 relative to avatar center
  3. VittoMoodService.updateGaze() updates the signal
  4. Component effect syncs to Rive gazeX/gazeY inputs
  5. Rive state machine offsets pupils accordingly

The gaze influence is mood-dependent (e.g., reduced during focused where scanning overrides, nearly zero during sleepy).


Conversation Integration

Connecting to Backend AI Mood Analysis

The backend AI API (backend-ai-api on port 3001) can analyze conversation context and return mood suggestions. Wire it up:

import { AI_API_URL } from '@libs-foundational/api-config';

@Injectable({ providedIn: 'root' })
export class ConversationMoodAnalyzer {
  private readonly aiApiUrl = inject(AI_API_URL);
  private readonly http = inject(HttpClient);
  private readonly moodService = inject(VittoMoodService);

  analyzeMood(messageContent: string): void {
    this.http.post<{ mood: VittoMood }>(
      `${this.aiApiUrl}/ai/analyze-mood`,
      { content: messageContent }
    ).subscribe(response => {
      this.moodService.setMood(response.mood);
    });
  }
}

Simple Rule-Based Mood Detection (No Backend)

For immediate mood feedback without waiting for API analysis:

function detectMoodFromKeywords(text: string): VittoMood {
  const lower = text.toLowerCase();

  if (/error|bug|crash|fail|broken/.test(lower)) return 'urgent';
  if (/sorry|apologize|mistake|my bad/.test(lower)) return 'apologetic';
  if (/amazing|awesome|perfect|great|love/.test(lower)) return 'celebratory';
  if (/think|analyze|consider|evaluate/.test(lower)) return 'analytical';
  if (/why|how|what if|wonder/.test(lower)) return 'curious';
  if (/help|understand|feel|frustrat/.test(lower)) return 'empathetic';
  if (/proud|nailed|accomplished|shipped/.test(lower)) return 'proud';
  if (/focus|concentrate|deep dive|debug/.test(lower)) return 'focused';
  if (/idea|eureka|realized|insight/.test(lower)) return 'inspired';
  if (/confused|unclear|don't understand/.test(lower)) return 'confused';
  if (/fun|play|joke|haha/.test(lower)) return 'playful';
  if (/happy|glad|yay|excited/.test(lower)) return 'cheerful';
  if (/tired|sleepy|break|rest/.test(lower)) return 'sleepy';

  return 'neutral';
}

Configuration

Custom .riv File Path

<ai-vitto-rive-avatar
  [rivFilePath]="'assets/rive/vitto-v2.riv'"
  [size]="160"
/>

Reduced Motion

The component automatically detects prefers-reduced-motion: reduce and sets animation intensity to 0. You can also control it programmatically:

this.moodService.setIntensity(0);   // disable all animations
this.moodService.setIntensity(0.5); // half intensity
this.moodService.setIntensity(1);   // full animations

Disable Gaze Tracking

<ai-vitto-rive-avatar [enableGazeTracking]="false" />

Rive File Requirements

The .riv file MUST contain:

Requirement Value
Artboard name VittoAvatar
State machine name MoodController
Input: moodIndex Number (0-13)
Input: gazeX Number (-1 to 1)
Input: gazeY Number (-1 to 1)
Input: intensity Number (0 to 1)
Input: isSpeaking Boolean

See RIVE-PRODUCTION-SPEC.md for complete animation specifications.


File Inventory

File Purpose
RIVE-PRODUCTION-SPEC.md Complete spec for building the .riv file
VITTO-ANIMATION-BIBLE.md Design philosophy and animation principles
vitto-rive-prototype.html Live SVG prototype for reference
angular/components/vitto-rive-avatar/ Angular standalone component
angular/services/vitto-mood.service.ts Mood management service
angular/types/vitto-mood.types.ts TypeScript type definitions
angular/index.ts Barrel export
INTEGRATION-GUIDE.md This document

Testing

Unit Tests (Vitest)

describe('VittoMoodService', () => {
  it('should default to neutral mood', () => {
    const service = TestBed.inject(VittoMoodService);
    expect(service.currentMood()).toBe('neutral');
  });

  it('should update mood index when mood changes', () => {
    const service = TestBed.inject(VittoMoodService);
    service.setMoodImmediate('cheerful');
    expect(service.currentMoodIndex()).toBe(2);
  });

  it('should clamp gaze position to -1..1', () => {
    const service = TestBed.inject(VittoMoodService);
    service.updateGaze({ x: 5, y: -3 });
    expect(service.gazePosition()).toEqual({ x: 1, y: -1 });
  });

  it('should track mood history', () => {
    const service = TestBed.inject(VittoMoodService);
    service.setMoodImmediate('focused');
    service.setMoodImmediate('cheerful');
    expect(service.moodHistory().length).toBe(2);
  });
});

E2E (Playwright)

test('Vitto avatar renders and responds to mood changes', async ({ page }) => {
  await page.goto('/chat');
  const avatar = page.locator('ai-vitto-rive-avatar');
  await expect(avatar).toBeVisible();
  await expect(avatar).toHaveAttribute('data-mood', 'neutral');

  // Trigger mood change
  await page.evaluate(() => {
    const service = (window as any).__vittoMoodService;
    service.setMoodImmediate('cheerful');
  });

  await expect(avatar).toHaveAttribute('data-mood', 'cheerful');
});