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
| Service | Purpose |
|---|
use-vicky-chat.ts | Chat hook for Vicky (Vicky agent, model mastra/vicky, endpoint /api/agents/vicky/stream) |
use-chat-stream.ts | Chat hook for Nous AI (model mastra/nous, endpoint /api/chat/stream) |
use-conversation-starter.ts | AI-generated conversation openers |
lib/api-client.ts | LMS 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'
}
| Field | Values | Description |
|---|
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
| Endpoint | Method | Purpose |
|---|
/lms/courses | GET | List published courses |
/lms/courses/:id | GET | Course detail with lessons |
/lms/enrollments/:userId | GET | User’s enrolled courses |
/lms/enroll | POST | Enroll in a course |
/lms/progress | POST | Update lesson progress |
/lms/video-url/:lessonId | GET | Get video streaming URL |
Direct Fetch (services/lms-service.ts) — currently used in app
| Endpoint | Method | Purpose |
|---|
/api/lms/courses | GET | List published courses |
/api/lms/courses/:courseId | GET | Course detail with lessons |
/api/lms/enrollments/:userId | GET | User’s enrolled courses |
/api/lms/enroll | POST | Enroll in a course |
/api/lms/progress | POST | Update lesson progress |
Messaging API Endpoints (services/matches-service.ts)
| Endpoint | Method | Purpose |
|---|
/api/matches | GET | List user’s matches (query: userId) |
/api/matches/:matchId | GET | Get specific match details |
/api/messages/:matchId | GET | Get messages for a match |
/api/messages/send | POST | Send 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 Value | Type | Description |
|---|
suggestions | string[] | Array of AI-generated conversation openers |
isLoading | boolean | Loading state while generating |
error | string | null | Friendly error message if generation fails |
generate | (matchName: string, sharedInterest?: string) => Promise<void> | Generate suggestions |
clear | () => void | Clear 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
Mock Mastra
Use MSW or similar to mock the /api/agents/vicky/stream endpoint
Stream verification
Test that streaming actually streams (not buffered)
Error states
Verify graceful degradation when Mastra is down
Memory context
Verify conversation history is maintained
Last Updated: 2026-05-13