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
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@radix-ui/react-tooltip": "^1.2.7",
"@t3-oss/env-nextjs": "^0.13.6",
"@tanstack/react-query": "^5.80.7",
"@tanstack/react-query-devtools": "^5.80.7",
"@tanstack/react-virtual": "^3.13.12",
"@trpc/client": "^11.4.0",
"@trpc/server": "^11.4.0",
Expand Down
5 changes: 1 addition & 4 deletions apps/web/src/ai/mastra/tools/semantic-journal-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ export const semanticJournalSearch = createTool({
description:
"Search the journal for entries that are semantically similar to a query",
execute: async ({ context }) => {
console.debug("[semanticJournalSearch] context 👀", context);

const results = await api.journal.getRelevantEntries({
limit: context.limit,
query: context.query,
Expand All @@ -17,7 +15,6 @@ export const semanticJournalSearch = createTool({
return results.map((result) => ({
content: result.embedding.chunk_markdown_text,
date: result.journal_entry.date,
id: result.journal_entry.id,
link: `/journal/${result.journal_entry.date}`,
similarity: result.similarity,
}));
Expand All @@ -35,7 +32,7 @@ export const semanticJournalSearch = createTool({
z.object({
content: z.string(),
date: z.string(),
id: z.string(),
link: z.string(),
similarity: z.number(),
}),
),
Expand Down
4 changes: 1 addition & 3 deletions apps/web/src/ai/mastra/tools/semantic-page-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ export const semanticPageSearch = createTool({
description:
"Search the pages for all entries that are semantically similar to a query. Returns multiple relevant results from different pages that should all be analyzed and synthesized.",
execute: async ({ context }) => {
console.debug("[semanticPageSearch] context 👀", context);

const result = await api.pages.getRelevantPageChunks({
const result = await api.pages.getRelevantPages({
limit: context.limit,
query: context.query,
threshold: context.threshold,
Expand Down
27 changes: 21 additions & 6 deletions apps/web/src/ai/mastra/tools/temporal-journal-search.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { ServerBlockNoteEditor } from "@blocknote/server-util";
import { createTool } from "@mastra/core/tools";
import { MDocument } from "@mastra/rag";
import { z } from "zod";
import { schema } from "~/components/editor/block-schema";
import { api } from "~/trpc/server";

export const temporalJournalSearch = createTool({
description: "Search the journal for entries between two dates",
execute: async ({ context }) => {
console.debug("[temporalJournalSearch] context 👀", context);

const results = await api.journal.getBetween({
from: context.from,
to: context.to,
});

return results.map((result) => ({
...result,
link: `/journal/${result.date}`,
}));
return await Promise.all(
results.map(async (result) => {
const editor = ServerBlockNoteEditor.create({
schema,
});
const markdown = result.document
? await editor.blocksToMarkdownLossy(result.document)
: "";
const mDocument = MDocument.fromMarkdown(markdown);
return {
content: mDocument.getText().join("\n"),
date: result.date,
id: result.id,
link: `/journal/${result.date}`,
};
}),
);
},
id: "temporal-journal-search",
inputSchema: z.object({
Expand All @@ -35,6 +49,7 @@ export const temporalJournalSearch = createTool({
content: z.string(),
date: z.string(),
id: z.string(),
link: z.string(),
}),
),
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function AppSidebarPageItem(props: AppSidebarPageItemProps) {
</Link>
{!!page && (
<DeletePageButton
className="hover:!bg-transparent hover:!text-destructive opacity-0 transition-opacity duration-200 group-hover/page-item:opacity-100"
className="!text-destructive !bg-transparent !pr-0 hidden group-hover/page-item:block"
page={page}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const AppSidebarPages = (props: AppSidebarPagesProps) => {
const { state, setOpen } = useSidebar();

const { data: pages } = useQuery({
...trpc.pages.getAll.queryOptions(),
...trpc.pages.getByUser.queryOptions(),
initialData: props.pages,
});

Expand Down Expand Up @@ -62,7 +62,7 @@ export const AppSidebarPages = (props: AppSidebarPagesProps) => {
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSub className="mr-0 pr-0">
<CreatePageButton />
{pages?.map((page) => (
<AppSidebarPageItem key={page.id} page={page} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function CreatePageButton() {
onSuccess: (newPage) => {
// Optimistically update the pages list
queryClient.setQueryData(
trpc.pages.getAll.queryOptions().queryKey,
trpc.pages.getByUser.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [newPage];
return [newPage, ...oldPages];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function DeletePageButton({ page, className }: DeletePageButtonProps) {

// Optimistically update the pages list
queryClient.setQueryData(
trpc.pages.getAll.queryOptions().queryKey,
trpc.pages.getByUser.queryOptions().queryKey,
(oldPages: Page[] | undefined) => {
if (!oldPages) return [];
return oldPages.filter((p) => p.id !== page.id);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/app/(app)/@appSidebar/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { AppSidebarUser } from "./_components/app-sidebar-user";
import { AppSidebarUserSkeleton } from "./_components/app-sidebar-user-skeleton";

async function SuspendedAppSidebarPages() {
const pages = await api.pages.getAll();
const pages = await api.pages.getByUser();
return <AppSidebarPages pages={pages} />;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { useTRPC } from "~/trpc/react";
const MIN_QUERY_LENGTH = 2;
const DEFAULT_THRESHOLD = 0.25;
const DEFAULT_LIMIT = 10;
const DEFAULT_DEBOUNCE_TIME = 500;
const DEFAULT_DEBOUNCE_TIME = 150;

type HeaderSearchButtonProps = React.ComponentProps<typeof Dialog> & {
children: React.ReactNode;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/src/app/(app)/@header/default.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Link from "next/link";
import { HeaderThemeToggle } from "~/app/(app)/@header/_components/header-theme-toggle";
import { SidebarTrigger } from "~/components/ui/sidebar";
import { HeaderCurrentDate } from "./_components/header-current-date";
Expand All @@ -11,7 +12,9 @@ export default function JournalHeader() {
<SidebarTrigger />
<div className="flex w-full items-center justify-between gap-x-2">
<div className="min-w-0 flex-1">
<HeaderCurrentDate />
<Link href="/journal">
<HeaderCurrentDate />
</Link>
</div>
<HeaderSearchButton>
<HeaderSearchTrigger />
Expand Down
48 changes: 26 additions & 22 deletions apps/web/src/app/(app)/journal/[date]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { notFound } from "next/navigation";
import { Suspense } from "react";
import { api } from "~/trpc/server";
import { JournalEntryEditor } from "../_components/journal-entry-editor";
import {
JournalEntryContent,
JournalEntryHeader,
JournalEntryProvider,
JournalEntryTextArea,
} from "../_components/journal-entry";
JournalEntryLink,
JournalEntryWrapper,
} from "../_components/journal-entry-primitives";
import { JournalEntryProvider } from "../_components/journal-entry-provider";
import { JournalEntrySkeleton } from "../_components/journal-entry-skeleton";

export default async function Page({
Expand All @@ -21,34 +23,36 @@ export default async function Page({
</Suspense>
);
}
async function SuspendedJournalEntry({ date }: { date: string }) {
const entry = await api.journal.getByDate({ date });

if (!entry) {
notFound();
}

function JournalEntryFallback({ date }: { date: string }) {
return (
<JournalEntryProvider
className="mx-auto h-full max-w-4xl px-4 py-8 md:px-8"
entry={entry}
className="mx-auto min-h-full max-w-5xl px-13.5 py-8"
entry={{ date }}
>
<JournalEntryHeader forceDate />
<JournalEntryContent>
<JournalEntryTextArea autoFocus />
</JournalEntryContent>
<JournalEntryHeader className="mb-6" />
<JournalEntrySkeleton hasHeader={false} hasContent={true} />
</JournalEntryProvider>
);
}

function JournalEntryFallback({ date }: { date: string }) {
async function SuspendedJournalEntry({ date }: { date: string }) {
const entry = await api.journal.getByDate({ date });

if (!entry) {
notFound();
}

return (
<JournalEntryProvider
className="mx-auto min-h-full max-w-4xl px-4 py-8 md:px-8"
entry={{ date }}
>
<JournalEntryHeader className="mb-2" forceDate />
<JournalEntrySkeleton hasHeader={false} hasContent={true} />
<JournalEntryProvider entry={entry}>
<JournalEntryWrapper className="mx-auto max-w-5xl pt-8 pb-20">
<JournalEntryLink>
<JournalEntryHeader className="px-13.5" />
</JournalEntryLink>
<JournalEntryContent>
<JournalEntryEditor />
</JournalEntryContent>
</JournalEntryWrapper>
</JournalEntryProvider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

import dynamic from "next/dynamic";

export const DynamicJournalEntryEditor = dynamic(
() => import("./journal-entry-editor").then((mod) => mod.JournalEntryEditor),
{
ssr: false,
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";

import type { BlockTransaction, TimelineEntry } from "@acme/api";
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";
import { useJournalEntry } from "./journal-entry-provider";

const DEFAULT_DEBOUNCE_TIME = 150;

type PageEditorProps = {
debounceTime?: number;
onCreate?: (newEntry: TimelineEntry) => void;
};

export function JournalEntryEditor({
debounceTime = DEFAULT_DEBOUNCE_TIME,
onCreate,
}: PageEditorProps) {
const trpc = useTRPC();
const pendingChangesRef = useRef<BlockTransaction[]>([]);
const { initialBlocks, documentId, date } = useJournalEntry();

const { mutate, isPending } = useMutation({
...trpc.journal.saveTransactions.mutationOptions({}),
// ! TODO: When the mutation fails we need to revert the changes to the editor just like Notion does.
// ! To do this we can use `onError` and `editor.undo()`, without calling the transactions. We might have to get creative.
// ! Maybe we can refetch the blocks after an error instead of `undo`?
onSuccess: (data) => {
if (pendingChangesRef.current.length > 0) {
debouncedMutate();
}
if (!documentId && data) {
onCreate?.(data);
}
},
});

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

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

debouncedMutate();
}

return (
<BlockEditor initialBlocks={initialBlocks} onChange={handleEditorChange} />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import Link from "next/link";
import type { ComponentProps } from "react";
import { cn } from "~/components/utils";
import { useJournalEntry } from "./journal-entry-provider";

type JournalEntryWrapperProps = ComponentProps<"div">;

export function JournalEntryWrapper({
className,
children,
...rest
}: JournalEntryWrapperProps) {
const { isToday } = useJournalEntry();

return (
<div
className={cn(isToday && "min-h-96 md:min-h-124", className)}
{...rest}
>
{children}
</div>
);
}

export function JournalEntryLink({ className, ...rest }: ComponentProps<"a">) {
const { date } = useJournalEntry();

return (
<Link
className={cn("text-muted-foreground", className)}
href={`/journal/${date}`}
{...rest}
/>
);
}

type JournalEntryHeaderProps = Omit<ComponentProps<"div">, "children"> & {
forceDate?: boolean;
};

export function JournalEntryHeader({
className,
forceDate = false,
...rest
}: JournalEntryHeaderProps) {
const { formattedDate, isToday } = useJournalEntry();

return (
<div className={cn("mb-6", className)} {...rest}>
<h2 className="font-semibold text-5xl text-muted-foreground">
{isToday && !forceDate ? "Today" : formattedDate}
</h2>
</div>
);
}

type JournalEntryContentProps = ComponentProps<"div">;

export function JournalEntryContent({
className,
children,
...rest
}: JournalEntryContentProps) {
return (
<div className={cn("flex items-start gap-2", className)} {...rest}>
<div className="min-w-0 flex-1">{children}</div>
</div>
);
}
Loading