The Mora chat interface provides a conversational way to interact with your baby care data. Understanding threads, messages, and context helps you get the most from Mora.
Thread Management
Mora organizes conversations into threads that maintain context and history.
Thread Structure
moraThreads : defineTable ({
babyId: v . optional ( v . id ( "babyProfiles" )),
title: v . string (),
status: v . string (), // "active" | "closed"
lastMessageAt: v . string (),
createdAt: v . string (),
}). index ( "by_babyId_lastMessageAt" , [ "babyId" , "lastMessageAt" ])
Creating Threads
Threads are automatically created when you open Mora. Each thread is associated with your current baby profile:
export const getOrCreateMoraThread = mutation ({
args: {
babyId: v . optional ( v . id ( "babyProfiles" )),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const babyId = args . babyId ??
( await getLatestBabyProfileIdForUser ( ctx , user . _id )) ?? undefined ;
// Look for existing active thread
const existing = babyId
? await ctx . db
. query ( "moraThreads" )
. withIndex ( "by_babyId_lastMessageAt" , ( q ) =>
q . eq ( "babyId" , babyId )
)
. order ( "desc" )
. take ( 1 )
: [];
if ( existing [ 0 ] && existing [ 0 ]. status === "active" ) {
return existing [ 0 ];
}
// Create new thread
const now = new Date (). toISOString ();
const threadId = await ctx . db . insert ( "moraThreads" , {
babyId ,
title: "Mora Assistant" ,
status: "active" ,
lastMessageAt: now ,
createdAt: now ,
});
return await ctx . db . get ( threadId );
},
});
Creation : Automatic when Mora sidebar opens
Active : Accumulates messages during conversation
Closed : When you start a new conversation or close Mora
Reuse : Existing active threads are reused when possible
Starting New Conversations
Click the New button in the Mora header to start a fresh conversation. This closes the current thread and creates a new one:
src/components/MoraSidebar.tsx
const handleStartNew = useCallback (() => {
setSessionKey (( k ) => k + 1 ); // Triggers new thread creation
}, []);
Message Types and Parts
Mora messages use a flexible parts-based structure that supports different content types.
Message Schema
moraMessages : defineTable ({
threadId: v . id ( "moraThreads" ),
role: v . string (), // "user" | "assistant" | "system"
parts: v . any (), // Array of message parts
text: v . optional ( v . string ()), // Plain text extraction
routeContext: v . optional ( v . object ({
pathname: v . string (),
pageLabel: v . string (),
})),
createdAt: v . string (),
}). index ( "by_threadId_createdAt" , [ "threadId" , "createdAt" ])
Message Parts
Messages consist of typed parts that represent different content:
Text Parts
Tool Call Parts
Tool Result Parts
{
type : "text" ,
text : "Your baby had 8 feeds yesterday"
}
{
type : "tool-call" ,
toolName : "createEvent" ,
args : {
type : "DIAPER" ,
timestamp : "2024-03-05T14:30:00Z"
}
}
{
type : "tool-result" ,
toolCallId : "call_abc123" ,
result : {
status : "executed" ,
entityId : "kg2h..."
}
}
Creating Messages
export const createMoraMessage = mutation ({
args: {
threadId: v . id ( "moraThreads" ),
role: v . string (),
parts: v . any (),
text: v . optional ( v . string ()),
routeContext: v . optional (
v . object ({
pathname: v . string (),
pageLabel: v . string (),
})
),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const thread = await ctx . db . get ( args . threadId );
if ( ! thread ) throw new Error ( "Thread not found" );
if ( thread . babyId ) {
await requireBabyAccess ( ctx , thread . babyId , user . _id );
}
const now = new Date (). toISOString ();
const id = await ctx . db . insert ( "moraMessages" , {
... args ,
createdAt: now ,
});
// Update thread timestamp
await ctx . db . patch ( args . threadId , {
lastMessageAt: now ,
});
return id ;
},
});
Listing Messages
const messages = useQuery ( api . mora . listMoraMessages , {
threadId ,
limit: 100 ,
cursor: undefined ,
});
Messages are returned in ascending order by createdAt, making it easy to display a chronological conversation.
Route Context and Contextual Help
Mora adapts to your current location in the app by using route context .
Context Structure
routeContext : {
pathname : string ; // "/", "/trends", "/reminders", etc.
pageLabel : string ; // "Today", "Trends", "Reminders", etc.
}
Context-Aware Prompts
Quick prompts change based on the current page:
src/components/MoraSidebar.tsx
const QUICK_PROMPTS : Record < string , string []> = {
Today: [ "Summarize the last 24h" , "What should I log next?" ],
Trends: [ "Analyze last 7 days" , "Any feeding patterns?" ],
Reminders: [ "Show upcoming reminders" , "Create a 9 AM vitamin reminder" ],
Records: [ "Find notes about rash" , "Summarize recent meds" ],
Settings: [ "Explain YOLO mode" , "What can Mora update?" ],
Unknown: [ "What can you help with?" ],
};
function getPageLabel ( pathname : string ) {
if ( pathname === "/" ) return "Today" ;
if ( pathname . startsWith ( "/trends" )) return "Trends" ;
if ( pathname . startsWith ( "/records" )) return "Records" ;
if ( pathname . startsWith ( "/reminders" )) return "Reminders" ;
if ( pathname . startsWith ( "/settings" )) return "Settings" ;
return "Unknown" ;
}
Passing Context to Mora
Context is automatically included in every API request:
src/components/MoraSidebar.tsx
const transport = useMemo (
() =>
new DefaultChatTransport ({
api: "/api/mora" ,
body: {
threadId ,
clientContext: {
pathname ,
pageLabel ,
timestamp: new Date (). toISOString (),
userName: session ?. user ?. name ?? undefined ,
userEmail: session ?. user ?. email ?? undefined ,
babyName: babyProfile ?. name ?? undefined ,
babyDob: babyProfile ?. dob ?? undefined ,
babyTimezone: babyProfile ?. timezone ?? undefined ,
familyName: familyName ?? undefined ,
},
},
}),
[ pathname , pageLabel , sessionKey , threadId ]
);
Route context helps Mora provide more relevant suggestions and understand implicit references like “this page” or “these trends.”
Component Architecture
The main container that manages the Mora experience:
src/components/MoraSidebar.tsx
interface MoraSidebarProps {
isOpen : boolean ;
onClose : () => void ;
}
export default function MoraSidebar ({ isOpen , onClose } : MoraSidebarProps ) {
const pathname = usePathname () ?? "/" ;
const pageLabel = getPageLabel ( pathname );
const settings = useQuery ( api . mora . getMoraSettings , {});
const [ sessionKey , setSessionKey ] = useState ( 0 );
// Keyboard shortcuts
useEffect (() => {
if ( ! isOpen ) return ;
const onKey = ( e : KeyboardEvent ) => {
if ( e . key === "Escape" ) onClose ();
};
window . addEventListener ( "keydown" , onKey );
return () => window . removeEventListener ( "keydown" , onKey );
}, [ isOpen , onClose ]);
const moraEnabled = settings ?. enabled ?? true ;
const yoloOn = settings ?. yoloMode ?? false ;
return (
< aside className = "fixed inset-y-0 right-0 z-50 w-full md:w-[520px] bg-[#FEFCF8]" >
{ /* Header with status badge */ }
< div className = "border-b border-black/5 px-4 md:px-5 py-3" >
< div className = "flex items-center justify-between gap-3" >
< div className = "flex items-center gap-2" >
< MoraOrb size = "sm" state = "idle" />
< div >
< h2 className = "text-base font-semibold" > Mora </ h2 >
< p className = "text-[11px] text-muted" > AI copilot · { pageLabel } </ p >
</ div >
</ div >
< span className = { yoloOn ? "bg-alert-red/8" : "bg-sage/8" } >
{ yoloOn ? "YOLO" : "Safe" }
</ span >
</ div >
</ div >
{ /* Thread with runtime provider */ }
{ moraEnabled ? (
< MoraRuntimeProvider pathname = { pathname } sessionKey = { sessionKey } >
< MoraThread quickPrompts = { QUICK_PROMPTS [ pageLabel ] } />
</ MoraRuntimeProvider >
) : (
< div > Mora is disabled </ div >
) }
</ aside >
);
}
MoraComposer Component
The input area for sending messages:
src/components/MoraComposer.tsx
interface MoraComposerProps {
disabled ?: boolean ;
busy ?: boolean ;
onSend : ( text : string ) => Promise < void > | void ;
}
export default function MoraComposer ({ disabled , busy , onSend } : MoraComposerProps ) {
const [ value , setValue ] = useState ( "" );
const handleSubmit = async ( e : FormEvent ) => {
e . preventDefault ();
const text = value . trim ();
if ( ! text || disabled || busy ) return ;
setValue ( "" );
await onSend ( text );
};
return (
< form onSubmit = { handleSubmit } >
< Textarea
value = { value }
onChange = { ( e ) => setValue ( e . target . value ) }
placeholder = "Ask Mora about feeds, sleep, reminders, or trends..."
disabled = { disabled || busy }
rows = { 3 }
/>
< Button type = "submit" disabled = { disabled || busy || ! value . trim () } >
Send
</ Button >
</ form >
);
}
MoraThread Component
The conversation display using Assistant UI primitives:
src/components/mora/MoraThread.tsx
export default function MoraThread ({ quickPrompts } : MoraThreadProps ) {
return (
<>
< ThreadPrimitive.Viewport autoScroll className = "flex-1 overflow-y-auto" >
< MoraWelcome quickPrompts = { quickPrompts } />
< ThreadPrimitive.Messages
components = { { UserMessage , AssistantMessage } }
/>
</ ThreadPrimitive.Viewport >
< MoraComposer />
</>
);
}
Advanced Features
Mora supports speech-to-text for hands-free interaction:
src/components/mora/MoraThread.tsx
function MoraComposer () {
const composerRuntime = useComposerRuntime ();
const handleVoiceTranscript = useCallback (
( text : string ) => {
composerRuntime . setText ( text );
composerRuntime . send ();
},
[ composerRuntime ]
);
return (
< ComposerPrimitive.Root >
< VoiceButton onTranscript = { handleVoiceTranscript } />
< ComposerPrimitive.Input placeholder = "Ask Mora or tap mic..." />
< ComposerPrimitive.Send />
</ ComposerPrimitive.Root >
);
}
Quick Suggestions
Tap contextual prompts to auto-send common queries:
< ThreadPrimitive.Suggestion
key = { prompt }
prompt = { prompt }
autoSend
asChild
>
< button type = "button" >
{ prompt }
</ button >
</ ThreadPrimitive.Suggestion >
Message Status Indicators
Visual feedback during processing:
function InProgressIndicator () {
const message = useMessage ();
if ( message . status ?. type !== "running" ) return null ;
return (
< div className = "flex items-center gap-1.5" >
< MoraOrb size = "xs" state = "thinking" />
< span > Thinking... </ span >
</ div >
);
}
Best Practices
Keep Conversations Focused
Start a new thread when switching contexts or baby profiles. This helps Mora provide more accurate and relevant responses.
Quick prompts are tailored to each page. Use them to discover what Mora can do in different parts of the app.
When creating events or reminders, include times, amounts, and other details in your message for better accuracy.
Scroll back through the thread to see what you’ve asked and what actions were taken. All messages are preserved.
Next Steps
Actions System Learn how Mora executes actions and modifies your data