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
7 changes: 3 additions & 4 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@
"@assistant-ui/react-ai-sdk": "^0.10.16",
"@assistant-ui/react-markdown": "^0.10.6",
"@assistant-ui/styles": "^0.1.14",
"@blocknote/core": "^0.33.0",
"@blocknote/mantine": "^0.33.0",
"@blocknote/react": "^0.33.0",
"@blocknote/core": "^0.35.0",
"@blocknote/mantine": "^0.35.0",
"@blocknote/react": "^0.35.0",
"@daveyplate/better-auth-ui": "^2.1.0",
"@hookform/resolvers": "^5.0.1",
"@mantine/core": "^8.1.3",
"@mantine/hooks": "^8.1.3",
"@mastra/core": "^0.12.1",
"@mastra/libsql": "^0.12.0",
"@mastra/loggers": "^0.10.5",
Expand Down
5 changes: 2 additions & 3 deletions apps/web/src/ai/mastra/agents/journl-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const AGENT_INSTRUCTIONS = () => {
return `
You are Journl, 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.

**Today's date is ${today}.**
Current date: ${today}

## Your Personality

Expand All @@ -36,8 +36,6 @@ When users mention their thoughts, experiences, or ask about patterns, you intui

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.

**The links to the journal entries and pages are always relative to the current page**.

## 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.
Expand All @@ -53,6 +51,7 @@ You always link what you find naturally: [brief description](/journal/YYYY-MM-DD
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function AppSidebarPageItem(props: AppSidebarPageItemProps) {
href={`/pages/${page?.id}`}
className="line-clamp-1 min-w-0 flex-1 truncate hover:underline"
>
{page?.title || "New Page"}
{page?.title || "New page"}
</Link>
{!!page && (
<DeletePageButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function CreatePageButton() {
createPage(
{
children: [],
title: "New Page",
title: "",
},
{
onError: (error) => {
Expand Down Expand Up @@ -64,7 +64,7 @@ export function CreatePageButton() {
disabled={showLoading}
>
<Plus />
{showLoading ? "Creating..." : "New Page"}
{showLoading ? "Creating..." : "New page"}
</Button>
</div>
</SidebarMenuSubButton>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export function DeletePageButton({ page, className }: DeletePageButtonProps) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

const { mutate: deletePage, isPending: isDeleting } = useMutation(
trpc.pages.delete.mutationOptions({}),
trpc.document.delete.mutationOptions({}),
);

const handleDelete = () => {
Expand All @@ -42,16 +42,14 @@ export function DeletePageButton({ page, className }: DeletePageButtonProps) {
const confirmDelete = () => {
startTransition(() => {
deletePage(
{ id: page.id },
{ id: page.document_id },
{
onError: (error) => {
console.error("Failed to delete page:", error);
},
onSuccess: (result) => {
const deletedPageId = result.deletedPage.id;

onSuccess: () => {
// Only navigate away if we're currently on the deleted page
if (pathname === `/pages/${deletedPageId}`) {
if (pathname === `/pages/${page.id}`) {
router.push("/journal");
}

Expand All @@ -60,18 +58,18 @@ export function DeletePageButton({ page, className }: DeletePageButtonProps) {
trpc.pages.getAll.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [];
return oldPages.filter((p) => p.id !== deletedPageId);
return oldPages.filter((p) => p.id !== page.id);
},
);

// Remove the specific page from cache
queryClient.removeQueries({
queryKey: trpc.pages.getById.queryKey({ id: deletedPageId }),
queryKey: trpc.pages.getById.queryKey({ id: page.id }),
});

// Cancel any in-flight queries for this page
queryClient.cancelQueries({
queryKey: trpc.pages.getById.queryKey({ id: deletedPageId }),
queryKey: trpc.pages.getById.queryKey({ id: page.id }),
});

// Close the dialog after successful deletion
Expand Down
39 changes: 27 additions & 12 deletions apps/web/src/app/(app)/pages/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { notFound } from "next/navigation";
import { api, prefetch, trpc } from "~/trpc/server";
import { PageEditor } from "../_components/page-editor";
import { Suspense } from "react";
import { api } from "~/trpc/server";
import { DynamicPageEditor } from "../_components/page-editor.dynamic";
import { PageTitleInput } from "../_components/page-title-input";

export default async function Page({
params,
Expand All @@ -15,14 +17,27 @@ export default async function Page({
notFound();
}

if (page?.children && page.children.length > 0) {
prefetch(
trpc.blocks.loadPageChunk.queryOptions({
limit: 100,
parentChildren: page.children,
}),
);
}

return <PageEditor id={id} />;
return (
<div className="flex h-full flex-col gap-4 p-4">
<div className="min-h-0 flex-1">
<PageTitleInput
page={{
id: page.id,
title: page.title,
}}
className="mb-4 pl-13"
/>
<Suspense>
<DynamicPageEditor
page={{
document_id: page.document_id,
id: page.id,
title: page.title,
}}
initialBlocks={page.document}
/>
</Suspense>
</div>
</div>
);
}
12 changes: 0 additions & 12 deletions apps/web/src/app/(app)/pages/_components/page-blocks.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions apps/web/src/app/(app)/pages/_components/page-editor.dynamic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import dynamic from "next/dynamic";

export const DynamicPageEditor = dynamic(
() => import("./page-editor").then((mod) => mod.PageEditor),
{
ssr: false,
},
);
50 changes: 42 additions & 8 deletions apps/web/src/app/(app)/pages/_components/page-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
"use client";

import { LazyBlockEditor } from "~/components/editor/lazy-block-editor";
import type { BlockTransaction } from "@acme/api";
import type { Page } from "@acme/db/schema";
import type { PartialBlock } from "@blocknote/core";
import { useMutation } from "@tanstack/react-query";
import { useRef } from "react";
import { useDebouncedCallback } from "use-debounce";
import { BlockEditor } from "~/components/editor/block-editor";
import { useTRPC } from "~/trpc/react";

type PageEditorProps = {
id: string;
page: Pick<Page, "id" | "title" | "document_id">;
initialBlocks: [PartialBlock, ...PartialBlock[]] | undefined;
debounceTime?: number;
};

export function PageEditor({ id }: PageEditorProps) {
export function PageEditor({
page,
initialBlocks,
debounceTime = 200,
}: PageEditorProps) {
const trpc = useTRPC();
const pendingChangesRef = useRef<BlockTransaction[]>([]);

const { mutate, isPending } = useMutation({
...trpc.blocks.saveTransactions.mutationOptions({}),
onSuccess: () => {
if (pendingChangesRef.current.length > 0) {
debouncedMutate();
}
},
});

const debouncedMutate = useDebouncedCallback(() => {
if (isPending) return;
const transactions = pendingChangesRef.current;
pendingChangesRef.current = [];
mutate({ document_id: page.document_id, transactions });
}, debounceTime);

function handleEditorChange(transactions: BlockTransaction[]) {
pendingChangesRef.current.push(...transactions);

debouncedMutate();
}

return (
<div className="flex h-full flex-col gap-4 p-4">
<div className="min-h-0 flex-1">
<LazyBlockEditor parentId={id} parentType="page" />
</div>
</div>
<BlockEditor initialBlocks={initialBlocks} onChange={handleEditorChange} />
);
}
110 changes: 110 additions & 0 deletions apps/web/src/app/(app)/pages/_components/page-title-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import type { Page } from "@acme/db/schema";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { Input } from "~/components/ui/input";
import { cn } from "~/components/utils";
import { useTRPC } from "~/trpc/react";

type PageEditorTitleProps = {
page: Pick<Page, "id" | "title">;
placeholder?: string;
className?: string;
debounceTime?: number;
onTitleChange?: (title: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
} & Omit<React.ComponentProps<"input">, "value" | "onChange" | "onKeyDown">;

export function PageTitleInput({
page,
placeholder = "New page",
className,
debounceTime = 150,
onTitleChange,
onKeyDown,
ref,
...rest
}: PageEditorTitleProps) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const { mutate: updatePageTitle } = useMutation(
trpc.pages.updateTitle.mutationOptions({}),
);
const [title, setTitle] = useState(page.title);

// Debounced API call for page title updates
const debouncedUpdate = useDebouncedCallback((newTitle: string) => {
// Optimistically update the cache
queryClient.setQueryData(
trpc.pages.getById.queryOptions({ id: page.id }).queryKey,
(old) => {
if (!old) return old;
return {
...old,
title: newTitle,
updatedAt: new Date().toISOString(),
};
},
);

// Update the pages.getAll query cache
queryClient.setQueryData(
trpc.pages.getAll.queryOptions().queryKey,
(old) => {
if (!old) return old;
return old.map((p) =>
p.id === page.id
? {
...p,
title: newTitle,
}
: p,
);
},
);

// Execute the mutation
updatePageTitle(
{ id: page.id, title: newTitle },
{
onError: () => {
// If the mutation fails, invalidate queries to refetch correct data
queryClient.invalidateQueries({
queryKey: trpc.pages.getById.queryOptions({ id: page.id }).queryKey,
});
queryClient.invalidateQueries({
queryKey: trpc.pages.getAll.queryOptions().queryKey,
});
},
},
);
}, debounceTime);

function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const newTitle = e.target.value;
setTitle(newTitle);
onTitleChange?.(newTitle);
debouncedUpdate(newTitle);
}

function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
onKeyDown?.(e);
}

return (
<Input
ref={ref}
value={title}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className={cn(
"!bg-transparent !outline-none !ring-0 !text-3xl border-none px-0 font-bold placeholder:text-muted-foreground/60",
className,
)}
{...rest}
/>
);
}
Loading
Loading