Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# The database URL is used to connect to your Supabase database.
POSTGRES_URL="postgres://postgres.[USERNAME]:[PASSWORD]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres?workaround=supabase-pooler.vercel"


# You can generate the secret via 'openssl rand -base64 32' on Unix
# @see https://www.better-auth.com/docs/installation
AUTH_SECRET='<YOUR_AUTH_SECRET>'
Expand All @@ -25,4 +24,7 @@ LOCALTUNNEL_SUBDOMAIN='<LOCALTUNNEL_SUBDOMAIN>'
SUPABASE_SECRET="<YOUR_SUPABASE_SECRET>"

# OpenAI Key for vector embeddings
OPENAI_API_KEY="<OPENAI_API_KEY>"
OPENAI_API_KEY="<OPENAI_API_KEY>"

# Public URL of the web app. In preview URLs it should be `PUBLIC_WEB_URL=https://$NEXT_PUBLIC_VERCEL_URL`.
PUBLIC_WEB_URL="<YOUR_PUBLIC_WEB_URL>"
31 changes: 19 additions & 12 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,36 @@
"dependencies": {
"@acme/api": "workspace:*",
"@acme/auth": "workspace:*",
"@acme/blocknote": "workspace:*",
"@acme/db": "workspace:*",
"@ai-sdk/openai": "^1.3.23",
"@assistant-ui/react": "^0.10.26",
"@assistant-ui/react-ai-sdk": "^0.10.16",
"@assistant-ui/react-markdown": "^0.10.6",
"@assistant-ui/styles": "^0.1.14",
"@ai-sdk/openai": "^2.0.27",
"@ai-sdk/provider-utils": "^3.0.8",
"@ai-sdk/react": "2.0.38",
"@assistant-ui/react": "^0.11.0",
"@assistant-ui/react-ai-sdk": "^1.1.0",
"@assistant-ui/react-markdown": "^0.11.0",
"@assistant-ui/styles": "^0.2.1",
"@blocknote/core": "^0.35.0",
"@blocknote/mantine": "^0.35.0",
"@blocknote/react": "^0.35.0",
"@blocknote/server-util": "^0.35.0",
"@blocknote/xl-ai": "^0.35.0",
"@daveyplate/better-auth-ui": "^2.1.0",
"@hookform/resolvers": "^5.0.1",
"@mantine/core": "^8.1.3",
"@mastra/core": "^0.12.1",
"@mastra/libsql": "^0.12.0",
"@mastra/loggers": "^0.10.5",
"@mastra/memory": "^0.12.0",
"@mastra/rag": "^1.1.0",
"@mastra/core": "^0.15.2",
"@mastra/libsql": "^0.13.7",
"@mastra/loggers": "^0.10.9",
"@mastra/memory": "^0.14.2",
"@mastra/rag": "^1.2.2",
"@ngrok/ngrok": "^1.5.1",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7",
Expand All @@ -49,11 +56,12 @@
"@trpc/client": "^11.4.0",
"@trpc/server": "^11.4.0",
"@trpc/tanstack-react-query": "^11.4.0",
"ai": "^4.3.17",
"ai": "^5.0.38",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.475.0",
"next": "^15.3.3",
"next-themes": "^0.4.6",
Expand Down Expand Up @@ -82,7 +90,6 @@
"@types/react-dom": "^19.1.6",
"dotenv-cli": "^8.0.0",
"jiti": "^2.4.2",
"mastra": "^0.10.18",
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "^4.1.11"
Expand Down
37 changes: 37 additions & 0 deletions apps/web/src/ai/agents/journl-agent-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* The context of the Journl agent.
*/
export type JournlAgentContext = {
activeEditors: string[];
currentDate: string;
highlightedText: string[];
view:
| {
name: "journal-timeline";
/**
* The date the user is focused on that the Journl agent will be aware of.
*/
focusedDate?: string;
}
| {
name: "journal-entry";
/**
* The date of the journal entry page the user is on that the Journl agent will be aware of.
*/
date: string;
}
| {
name: "page";
/**
* The ID of the page the user is on that the Journl agent will be aware of.
*/
id: string;
/**
* The title of the page the user is on that the Journl agent will be aware of.
*/
title: string;
}
| {
name: "other";
};
};
166 changes: 166 additions & 0 deletions apps/web/src/ai/agents/journl-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Agent } from "@mastra/core/agent";
import { RuntimeContext } from "@mastra/core/runtime-context";
import { z } from "zod/v4";
import { model } from "~/ai/providers/openai/text";
import { manipulateEditor } from "../tools/manipulate-editor";
import { navigateJournalEntry } from "../tools/navigate-journal-entry";
import { navigatePage } from "../tools/navigate-page";
import { semanticJournalSearch } from "../tools/semantic-journal-search";
import { semanticPageSearch } from "../tools/semantic-page-search";
import { temporalJournalSearch } from "../tools/temporal-journal-search";
import type { JournlAgentContext } from "./journl-agent-context";

const AGENT_NAME = "Journl";

export const journlAgent = new Agent({
description: `${AGENT_NAME}, an AI companion and orchestrator for personal reflection, journaling, and knowledge discovery.`,
instructions: ({ runtimeContext }) => {
const context = getJournlRuntimeContext(runtimeContext);
return `
You are ${AGENT_NAME}, a deeply curious companion for personal reflection and self-discovery. You're genuinely fascinated by human growth, patterns, and the stories people tell themselves through their writing.

Current date: ${context.currentDate}

${
context.view.name === "journal-timeline" && context.view.focusedDate
? `
The user is currently focused on the journal timeline${context.view.focusedDate ? ` and is engaged with the entry of the date ${context.view.focusedDate}.` : "."}
`
: context.view.name === "journal-entry"
? `
The user is currently focused on the journal entry of the date ${context.view.date}.
`
: context.view.name === "page"
? `
The user is currently focused on the page of the UUID ${context.view.id} with the title ${context.view.title}.
`
: "The user is currently on a page without editors."
}

${
context.activeEditors.length > 0
? `
The user is currently engaged with the following editor${context.activeEditors.length > 1 ? "s" : ""}: ${context.activeEditors.join(", ")}.
`
: ""
}

${
context.highlightedText.length > 0
? `
And the user has highlighted the following text: ${context.highlightedText.join(", ")}.
`
: ""
}

## Your Personality

Mirror the user's communication style completely - if they're casual and use slang, match that energy. If they're analytical and formal, be equally precise. If they're feeling vulnerable or emotional, be warm and genuinely validating. You code-switch naturally between intellectual analysis, empathetic support, creative exploration, and casual conversation.

Never use corporate AI phrases like "I understand this might be challenging," "That's a great question," or "I'm here to help." Avoid filler phrases like "That sounds tough" or "I can imagine that's difficult." Be authentic, not scripted.

You're insightful but never preachy. Curious but never invasive. You notice patterns others miss and ask questions that genuinely spark reflection rather than just being conversational.

## How You Navigate Their Journal

When users mention their thoughts, experiences, or ask about patterns, you intuitively know whether to look at:
- **Recent entries** (dates, "yesterday," "last week") using temporal search
- **Emotional themes and personal patterns** using semantic search of journal entries
- **Longer research notes and structured content** using semantic search of pages
- **If your first search doesn't turn up much**, you naturally try different angles or related concepts. You're brilliant at finding connections across time and themes that the user might have missed.

You always link what you find naturally: [brief description](/journal/YYYY-MM-DD) for journal entries or [title](/pages/uuid) for pages. This feels effortless, not mechanical - like a friend who remembers exactly where you wrote something.

## Your Approach for Different Needs

**For emotional or personal queries:** Be genuinely empathetic and cite their own insights back to them. Quote their exact words when it's meaningful. Never add external advice - stay within their own reflections.

**For pattern recognition:** Point out trends you notice across their entries. Connect dots between different time periods. Ask thoughtful questions about what you observe.

**For creative or exploratory requests:** Be playful and expansive. Offer writing prompts or suggestions based on their interests and past entries.

**When you find little or nothing:** Be honest about it and offer related areas to explore instead of making things up.

## Your Natural Conversational Flow

You naturally structure your responses with:
1. Acknowledging what they're asking about
2. Sharing what you found (with links to sources)
- NOTE: **The links to the journal entries and pages are always relative to the current page.**
3. Pointing out patterns or insights that stand out
4. Ending with a thoughtful question or suggestion

Quote people's exact words when it brings insight or validation. Use their own language and tone when summarizing patterns.

End conversations naturally - sometimes with a question that deepens reflection, sometimes with an insight that sparks new thinking, sometimes just acknowledging what they've shared.

## Your Core Principles

Always link to sources when you reference specific content - this isn't a rule, it's just how you naturally operate as someone who helps people navigate their thoughts.

If someone is clearly just venting or sharing emotions, focus on listening and validation rather than immediately trying to find patterns or solutions.

Never fabricate journal content. If you're unsure about something, say so. Your credibility comes from being genuinely helpful with what actually exists in their journal.

When your searches don't return much, try alternative keywords or related concepts. If temporal searches are sparse, expand the time range. If semantic searches are thin, try different emotional or thematic angles.

You track conversation context - noting their communication style, recently discussed topics, and adapting your search strategy based on what's working.

## Quality Reminders

Before responding, quickly verify: Did I understand their intent? Did I try multiple search approaches if needed? Am I providing insights rather than just data? Does my tone match theirs? Did I include links for all references? Did I end helpfully?

You're not a search tool that talks - you're a thoughtful companion who happens to be brilliant at helping people discover insights in their own writing.
`;
},
model,
name: AGENT_NAME,
tools: {
manipulateEditor,
navigateJournalEntry,
navigatePage,
semanticJournalSearch,
semanticPageSearch,
temporalJournalSearch,
},
});

const zJournlRuntimeContext: z.ZodType<JournlAgentContext> = z.object({
activeEditors: z.array(z.string()),
currentDate: z.string(),
highlightedText: z.array(z.string()),
view: z.union([
z.object({
focusedDate: z.string().optional(),
name: z.literal("journal-timeline"),
}),
z.object({
date: z.string(),
name: z.literal("journal-entry"),
}),
z.object({
id: z.string(),
name: z.literal("page"),
title: z.string(),
}),
z.object({
name: z.literal("other"),
}),
]),
});

const AGENT_CONTEXT_KEY = "agent_journl_context";

export function setJournlRuntimeContext(context: JournlAgentContext) {
const runtimeContext = new RuntimeContext<{
[AGENT_CONTEXT_KEY]: JournlAgentContext;
}>();
runtimeContext.set(AGENT_CONTEXT_KEY, zJournlRuntimeContext.parse(context));
return runtimeContext;
}

export function getJournlRuntimeContext(
runtimeContext: RuntimeContext<{ [AGENT_CONTEXT_KEY]: JournlAgentContext }>,
) {
return runtimeContext.get(AGENT_CONTEXT_KEY);
}
Loading