AI Integration

Doc Status: Excellent | ✓ Clear summary | ✓ Easy to read | ✓ Matches code | ✓ Good structure | ✓ Professional look | ✓ Visual components | ✓ No red herrings
Group chat supersession: Match and group chat AI no longer follow this Mastra-first architecture. They use the Convex-native Agent architecture documented in Convex Group Chat AI. Keep this page for direct Vicky one-on-one chat and other legacy Mastra-backed AI paths until those features are separately migrated.
Legacy Mastra guidance: For the remaining Mastra-backed AI features, use the AI SDK with Mastra as the backend runtime. This requirement does not apply to Convex-native match/group chat.

Why This Matters

Nous’s AI features (Vicky wingman, conversation suggestions, reflection insights) must feel human and enhance authenticity. The AI SDK + Mastra combination provides:
  • Streaming responses — Real-time AI output feels more natural
  • Loading states — Proper UX during AI processing
  • Error handling — Graceful degradation when AI is unavailable
  • Centralized AI logic — All AI runs in Mastra, not scattered across clients
  • Memory integration — Mastra agents can maintain conversation context

Architecture

Key Services

ServicePurpose
use-vicky-chat.tsChat hook for Vicky (Vicky agent, model mastra/vicky, endpoint /api/agents/vicky/stream)
use-chat-stream.tsChat hook for Nous AI (model mastra/nous, endpoint /api/chat/stream)
use-conversation-starter.tsAI-generated conversation openers
lib/api-client.tsLMS API with TanStack Query

Vicky AI Wingman

Hook Usage (hooks/use-vicky-chat.ts)

import { useVickyChat } from '~/hooks/use-vicky-chat'

function VickyChatScreen() {
  const { messages, sendMessage, isLoading, isAiEnabled, setIsAiEnabled } = useVickyChat()

  const handleSend = (content: string) => {
    sendMessage(content)
  }

  return (
    <>
      {/* Messages list */}
      {messages.map(msg => (
        <MessageBubble key={msg.id} message={msg} />
      ))}
      {/* Input */}
      <MessageInput onSend={handleSend} disabled={isLoading} />
    </>
  )
}

Alternative Chat Hook (hooks/use-chat-stream.ts)

For Nous AI (separate from Vicky), use useChatStream:
import { useChatStream } from '~/hooks/use-chat-stream'

function NousChatScreen() {
  const { messages, sendMessage, isLoading } = useChatStream({
    onError: (error) => consola.error('Chat error', { error }),
  })

  // ...
}

Streaming Implementation

Vicky uses streamText from the AI SDK for streaming responses:
const result = await streamText({
  model: 'mastra/vicky',
  system: 'You are Vicky, an AI wingman...',
  messages: conversationHistory,
  api: `${MASTRA_URL}/api/agents/vicky/stream`,
})

const text = await result.text // Accumulates stream

Message Types

type ChatMessage = {
  id: string
  role: 'user' | 'assistant'
  content: string
  sender: 'user' | 'vicky' | 'staff'
}
FieldValuesDescription
role'user' | 'assistant'Standard AI SDK role
sender'user' | 'vicky' | 'staff'Nous-specific sender identification
Two chat streams exist: useVickyChat (in use-vicky-chat.ts) routes to Vicky agent at /api/agents/vicky/stream using model mastra/vicky. useChatStream (in use-chat-stream.ts) routes to Nous AI at /api/chat/stream using model mastra/nous. Both use the AI SDK’s streamText.

API Endpoints

Vicky Streaming Endpoint

POST /api/agents/vicky/stream
Request:
{
  "messages": [{ "role": "user", "content": "What should I say?" }]
}
Response: Server-Sent Events (SSE) stream

LMS API Endpoints (Mastra)

Two implementations exist with different URL patterns: - lib/api-client.ts uses TanStack Query hooks with /lms/* paths (defined but not yet integrated) - services/lms-service.ts uses direct fetch with /api/lms/* paths (currently used) Both point to the same Mastra instance. Direct fetch is used instead of Fern because the OpenAPI spec from Mastra doesn’t include response schemas, causing Fern to generate methods that return void.
TanStack Query Hooks (lib/api-client.ts) — defined but not yet integrated into components
EndpointMethodPurpose
/lms/coursesGETList published courses
/lms/courses/:idGETCourse detail with lessons
/lms/enrollments/:userIdGETUser’s enrolled courses
/lms/enrollPOSTEnroll in a course
/lms/progressPOSTUpdate lesson progress
/lms/video-url/:lessonIdGETGet video streaming URL
Direct Fetch (services/lms-service.ts) — currently used in app
EndpointMethodPurpose
/api/lms/coursesGETList published courses
/api/lms/courses/:courseIdGETCourse detail with lessons
/api/lms/enrollments/:userIdGETUser’s enrolled courses
/api/lms/enrollPOSTEnroll in a course
/api/lms/progressPOSTUpdate lesson progress

Messaging API Endpoints (services/matches-service.ts)

EndpointMethodPurpose
/api/matchesGETList user’s matches (query: userId)
/api/matches/:matchIdGETGet specific match details
/api/messages/:matchIdGETGet messages for a match
/api/messages/sendPOSTSend a message

Configuration

Environment Variables

EXPO_PUBLIC_MASTRA_URL=https://nous-mastra.curlynguyen95.workers.dev

Mastra URL Fallback

const MASTRA_URL =
  process.env.EXPO_PUBLIC_MASTRA_URL ?? 'https://nous-mastra.curlynguyen95.workers.dev'

Patterns

AI Toggle

Users can disable AI features for pure 1-on-1 connection:
const [isAiEnabled, setIsAiEnabled] = useState(true)

const sendMessage = async (content: string) => {
  if (!isAiEnabled) {
    // Send as regular message
    return
  }
  // Stream AI response
}

Conversation Suggestions

The useConversationStarter hook generates AI-powered conversation openers:
import { useConversationStarter } from '~/hooks/use-conversation-starter'

const { suggestions, isLoading, error, generate, clear } = useConversationStarter()

// Generate suggestions for a match
await generate(matchName, sharedInterest)
Return ValueTypeDescription
suggestionsstring[]Array of AI-generated conversation openers
isLoadingbooleanLoading state while generating
errorstring | nullFriendly error message if generation fails
generate(matchName: string, sharedInterest?: string) => Promise<void>Generate suggestions
clear() => voidClear suggestions and error state

Error Handling

AI errors are logged but don’t crash the UI:
try {
  const result = await streamText({ ... })
} catch (error) {
  consola.error('Vicky chat error', { error })
  // User sees "AI unavailable" state
}

Anti-Patterns

Fern is forbidden for AI features. The Fern client cannot be used for AI endpoints — all AI features must use the AI SDK with Mastra streaming endpoints. This is a hard requirement.

Never Use Fern for AI

Fern generates void-returning methods when OpenAPI specs lack response schemas. AI features require streaming responses via AI SDK:
// WRONG - Fern client for AI (generates void methods)
const { data } = client.ai.sendMessageToAiWingman({ ... })

// CORRECT - AI SDK with Mastra streaming
const { messages, sendMessage } = useVickyChat()

Never Block UI on AI

// WRONG - Await in render
const response = await streamText({ ... }) // Blocks render

// CORRECT - Stream with loading state
const { messages, sendMessage, isLoading } = useVickyChat()

Testing AI Features

1

Mock Mastra

Use MSW or similar to mock the /api/agents/vicky/stream endpoint
2

Stream verification

Test that streaming actually streams (not buffered)
3

Error states

Verify graceful degradation when Mastra is down
4

Memory context

Verify conversation history is maintained

Last Updated: 2026-05-13