Skip to main content
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

convex/schema.ts
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:
convex/mora.ts
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);
  },
});
  1. Creation: Automatic when Mora sidebar opens
  2. Active: Accumulates messages during conversation
  3. Closed: When you start a new conversation or close Mora
  4. 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

convex/schema.ts
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:
{
  type: "text",
  text: "Your baby had 8 feeds yesterday"
}

Creating Messages

convex/mora.ts
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

MoraSidebar Component

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

Voice Input

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

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

Build docs developers (and LLMs) love