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
17 changes: 12 additions & 5 deletions apps/web/src/ai/tools/manipulate-editor.client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"use client";

import { getAIExtension } from "@blocknote/xl-ai";
import { useDrawer } from "~/components/ui/drawer";
import { useJournlAgentAwareness } from "../agents/use-journl-agent-awareness";
import { createClientTool } from "../utils/create-client-tool";
import { zManipulateEditorInput } from "./manipulate-editor.schema";

export function useManipulateEditorTool() {
const { getEditors } = useJournlAgentAwareness();
const { closeDrawer } = useDrawer();
const tool = createClientTool({
execute: async (toolCall, chat) => {
try {
Expand All @@ -25,7 +27,7 @@ export function useManipulateEditorTool() {

const aiExtension = getAIExtension(editor);

const result = await aiExtension.callLLM({
const response = await aiExtension.callLLM({
onBlockUpdate: (block) => {
const blockElement = document.querySelector(`[data-id="${block}"]`);
if (blockElement && !isElementPartiallyInViewport(blockElement)) {
Expand All @@ -35,15 +37,20 @@ export function useManipulateEditorTool() {
userPrompt: toolCall.input.userPrompt,
});

const changes = await result?.llmResult.streamObjectResult?.object;
const stream = response?.llmResult.streamObjectResult;
const changes = await stream?.object;

void chat.addToolResult({
output: `The following changes were made to the editor: ${JSON.stringify(
changes,
)}`,
output: changes
? `The following changes were made: ${JSON.stringify(changes)}`
: "Something went wrong and no changes were made.",
tool: toolCall.toolName,
toolCallId: toolCall.toolCallId,
});

if (changes) {
closeDrawer();
}
} catch (error) {
void chat.addToolResult({
output: `Error when calling the tool: ${error}`,
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/ai/tools/navigate-journal-entry.client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { useRouter } from "next/navigation";
import { useDrawer } from "~/components/ui/drawer";
import { createClientTool } from "../utils/create-client-tool";
import { zNavigateJournalEntryInput } from "./navigate-journal-entry.schema";

export function useNavigateJournalEntryTool() {
const router = useRouter();
const { closeDrawer } = useDrawer();
const tool = createClientTool({
execute: (toolCall, chat) => {
const entry = `/journal/${toolCall.input.date}`;
Expand All @@ -15,6 +17,7 @@ export function useNavigateJournalEntryTool() {
tool: toolCall.toolName,
toolCallId: toolCall.toolCallId,
});
closeDrawer();
},
inputSchema: zNavigateJournalEntryInput,
name: "navigateJournalEntry",
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/ai/tools/navigate-page.client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { useRouter } from "next/navigation";
import { useDrawer } from "~/components/ui/drawer";
import { createClientTool } from "../utils/create-client-tool";
import { zNavigatePageInput } from "./navigate-page.schema";

export function useNavigatePageTool() {
const router = useRouter();
const { closeDrawer } = useDrawer();
const tool = createClientTool({
execute: (toolCall, chat) => {
const page = `/pages/${toolCall.input.id}`;
Expand All @@ -15,6 +17,7 @@ export function useNavigatePageTool() {
tool: toolCall.toolName,
toolCallId: toolCall.toolCallId,
});
closeDrawer();
},
inputSchema: zNavigatePageInput,
name: "navigatePage",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export async function AppSidebarUser() {
</SidebarMenuButton>
</DropdownMenuTrigger>
<AppSidebarUserMenu
data-name="app-sidebar-user-menu"
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
align="end"
sideOffset={4}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/app/(app)/@chatDrawer/default.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export default function ChatDrawer() {
</div>
</div>
</DrawerTrigger>
<DrawerContent className="!h-full !max-h-[90dvh]">
<DrawerContent className="!h-full !max-h-[90dvh] z-4500">
<DrawerTitle className="hidden">Journl</DrawerTitle>
<div className="!h-full relative">
<DrawerDivider className="-translate-x-1/2 absolute top-0 left-1/2 z-50" />
<DrawerDivider className="-translate-x-1/2 absolute top-0 left-1/2 z-4500" />
<ThreadPrimitive.Root
className="relative box-border flex h-full flex-col overflow-hidden bg-sidebar pt-6"
style={{
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/app/(app)/globals.css
Original file line number Diff line number Diff line change
@@ -1 +1,30 @@
@import "../styles/blocknote.css";

/*
Z-Index Hierarchy

The following is the z-index hierarchy for the app.
The only component we need to edit in this file is the overlay.
The rest of the components are set by the Tailwind CSS classes, or by our editor framework.

- Sidebar Drag Handles (`z-index`: 6000)
- Auth Settings Modal (`z-index`: 5000)
- Header Search Modal and Overlay (`z-index`: 5000)
- Auth Settings Dropdown (`z-index`: 4500)
- Drawer (with its overlay and drag handles)(`z-index`: 4500)
- Editor Options/Drag Handle (`z-index`: 4000)
- Editor Toolbar (`z-index`: 3000)
- Editor AI Review (`z-index`: 3000)
- Editor Menu (`z-index`: 2000)
- Thread Composer Sources (`z-index`: 1600)
- Sidebars (`z-index`: 1500)
- Header (none)
- Main Content (none)
*/
/* NOTE: This affects all dialog overlays */
[data-slot="dialog-overlay"] {
z-index: 5000;
}
[data-radix-popper-content-wrapper]:has([data-name="app-sidebar-user-menu"]) {
z-index: 4500 !important;
}
45 changes: 17 additions & 28 deletions apps/web/src/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { ThemeProvider } from "next-themes";
import { withAuth } from "~/auth/guards";
import { SidebarInset, SidebarProvider } from "~/components/ui/sidebar";
import { Toaster } from "~/components/ui/toast";
import { TRPCReactProvider } from "~/trpc/react";
import ChatSidebarTrigger from "./@chatSidebar/_components/chat-sidebar-trigger";
import "./globals.css";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
Expand All @@ -25,33 +23,24 @@ function AppLayout({
header,
}: AppLayoutProps) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<AppProviders>
<TRPCReactProvider>
<SidebarProvider className="flex min-h-screen-safe flex-col">
<div className="flex flex-1">
<SidebarProvider defaultOpen={false}>
{appSidebar}
<SidebarInset className="flex max-h-svh flex-col">
{header}
<div className="min-w-54 flex-1 overflow-auto">
{children}
</div>
<div className="mt-auto">{chatDrawer}</div>
</SidebarInset>
</SidebarProvider>
{chatSidebar}
<ChatSidebarTrigger />
</div>
<AppProviders>
<SidebarProvider className="flex min-h-screen-safe flex-col">
<div className="flex flex-1">
<SidebarProvider defaultOpen={false}>
{appSidebar}
<SidebarInset className="flex max-h-svh flex-col">
{header}
<div className="min-w-54 flex-1 overflow-auto">{children}</div>
<div className="mt-auto">{chatDrawer}</div>
</SidebarInset>
</SidebarProvider>
<Toaster />
<ReactQueryDevtools
buttonPosition="bottom-left"
initialIsOpen={false}
/>
</TRPCReactProvider>
</AppProviders>
</ThemeProvider>
{chatSidebar}
<ChatSidebarTrigger />
</div>
</SidebarProvider>
<Toaster />
<ReactQueryDevtools buttonPosition="bottom-left" initialIsOpen={false} />
</AppProviders>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export async function AuthView({ pathname }: { pathname: string }) {
return (
<AuthCard
pathname={pathname}
// className="flex w-full max-w-sm flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm"

className="z-10 w-full flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm"
classNames={{
base: "bg-transparent border-none",
Expand All @@ -38,6 +36,7 @@ export async function AuthView({ pathname }: { pathname: string }) {
trigger: "md:hidden",
},
sidebar: {
base: "gap-y-2",
button: "cursor-pointer text-primary",
buttonActive:
"cursor-pointer border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
Expand Down
33 changes: 21 additions & 12 deletions apps/web/src/app/_components/app-providers.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
"use client";

import { MantineProvider } from "@mantine/core";
import { ThemeProvider } from "next-themes";
import { JournlAgentAwarenessProvider } from "~/ai/agents/use-journl-agent-awareness";
import { ThreadRuntime } from "~/components/assistant-ui/thread-runtime";
import { DrawerProvider } from "~/components/ui/drawer";
import { TRPCReactProvider } from "~/trpc/react";

export function AppProviders({ children }: { children: React.ReactNode }) {
return (
<MantineProvider>
<JournlAgentAwarenessProvider>
<ThreadRuntime
transport={{
api: "/api/ai/journl-agent",
}}
messages={[]}
>
{children}
</ThreadRuntime>
</JournlAgentAwarenessProvider>
</MantineProvider>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<MantineProvider>
<JournlAgentAwarenessProvider>
<TRPCReactProvider>
<DrawerProvider>
<ThreadRuntime
transport={{
api: "/api/ai/journl-agent",
}}
messages={[]}
>
{children}
</ThreadRuntime>
</DrawerProvider>
</TRPCReactProvider>
</JournlAgentAwarenessProvider>
</MantineProvider>
</ThemeProvider>
);
}
92 changes: 73 additions & 19 deletions apps/web/src/app/api/ai/blocknote/[...all]/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
import type { NextRequest } from "next/server";
import z from "zod";
import { handler as corsHandler } from "~/app/api/_cors/cors";
import { getSession } from "~/auth/server";
import { env } from "~/env";
import { api } from "~/trpc/server";

// import { api } from "~/trpc/server";
const OPENAI_MESSAGE_PREFIX = "data: ";
const OPENAI_MESSAGE_DONE_TEXT = "[DONE]";
const OPENAI_MODEL_PROVIDER = "openai";

const OPENAI_API_URL = "https://api.openai.com/v1/";
const zChatCompletionMessage = z.object({
choices: z.array(z.unknown()),
created: z.number(),
id: z.string(),
model: z.string(),
obfuscation: z.string(),
object: z.literal("chat.completion.chunk"),
service_tier: z.string(),
system_fingerprint: z.string(),
usage: z
.object({
completion_tokens: z.number(),
completion_tokens_details: z.object({
accepted_prediction_tokens: z.number(),
audio_tokens: z.number(),
reasoning_tokens: z.number(),
rejected_prediction_tokens: z.number(),
}),
prompt_tokens: z.number(),
prompt_tokens_details: z.object({
audio_tokens: z.number(),
cached_tokens: z.number(),
}),
total_tokens: z.number(),
})
.nullable(),
});

async function handler(req: NextRequest) {
const session = await getSession();
Expand All @@ -22,7 +52,7 @@ async function handler(req: NextRequest) {
return new Response("Not found", { status: 404 });
}

const openAIResponse = await fetch(new URL(url, OPENAI_API_URL), {
const openAIResponse = await fetch(new URL(url, env.OPENAI_API_URL), {
body: JSON.stringify({
...requestBody,
stream: true,
Expand All @@ -40,22 +70,46 @@ async function handler(req: NextRequest) {

const transformStream = new TransformStream({
async transform(chunk, controller) {
const text = new TextDecoder().decode(chunk);

// TODO: Parse chunk and determine if it's a usage chunk.
console.log("Intercepted chunk:", text);

// ! TODO: Track usage.
// api.usage.trackModelUsage({
// model: requestBody.model,
// provider: requestBody.provider,
// user_id: session.user.id,
// quantity: requestBody.stream_options.include_usage ? 1 : 0,
// unit: "output_tokens",
// });

// TODO: Do not forward the usage chunk.
controller.enqueue(chunk);
try {
const text = new TextDecoder().decode(chunk);

if (text === `${OPENAI_MESSAGE_PREFIX}${OPENAI_MESSAGE_DONE_TEXT}`) {
return controller.enqueue(chunk);
}

// Parsing the messages from the openAI response by removing the newline characters and the prefix.
const data = text
.replace(/[\r\n]+/g, "")
.trim()
.split(OPENAI_MESSAGE_PREFIX)
.filter((t) => t !== "" && t !== OPENAI_MESSAGE_DONE_TEXT);

for (const json of data) {
const message = zChatCompletionMessage.parse(JSON.parse(json));

if (!message.usage) continue;

await api.usage.trackModelUsage({
metrics: [
{
quantity: message.usage.prompt_tokens,
unit: "input_tokens",
},
{
quantity: message.usage.completion_tokens,
unit: "output_tokens",
},
],
model_id: message.model,
model_provider: OPENAI_MODEL_PROVIDER,
user_id: session.user.id,
});
}

controller.enqueue(chunk);
} catch (error) {
console.error("Error tracking model usage", error);
}
},
});

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/assistant-ui/thread-sources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function ComposerSources({ className }: ComposerSourcesProps) {
+{dropdown.length}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="z-2000 space-y-2 p-2">
<DropdownMenuContent className="z-1600 space-y-2 p-2">
<div className="text-muted-foreground text-xs">Selections</div>
<ScrollArea className="flex h-full max-h-42 flex-col [&_div:has(button)]:space-y-1">
{dropdown.map((source) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/auth/auth-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function AuthModal({ children }: AuthModalProps) {
<DialogTitle className="sr-only">
{getScreenReaderContent(pathname)}
</DialogTitle>
<DialogContent className="flex w-full items-center justify-center border bg-sidebar">
<DialogContent className="z-5000 flex w-full items-center justify-center border bg-sidebar">
{children}
</DialogContent>
</Dialog>
Expand Down
Loading