From 2d35a4683b33018fac1ca71594db5e90744bb195 Mon Sep 17 00:00:00 2001 From: Avraham Sakal Date: Sun, 17 Aug 2025 19:32:24 -0400 Subject: [PATCH] begin migration to trpc/react-query integration --- layouts/LayoutDefault.tsx | 329 ++++++++++++++++++++++---------------- package.json | 6 +- pages/chat/+Page.tsx | 41 +++++ pages/chat/@id/+Page.tsx | 24 +-- pages/chat/trpc.ts | 19 ++- pages/todo/TodoList.tsx | 4 +- pnpm-lock.yaml | 64 ++++++-- trpc/client.ts | 22 +-- trpc/server.ts | 13 +- 9 files changed, 348 insertions(+), 174 deletions(-) create mode 100644 pages/chat/+Page.tsx diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx index 27a5f4f..f10db97 100644 --- a/layouts/LayoutDefault.tsx +++ b/layouts/LayoutDefault.tsx @@ -22,165 +22,222 @@ import { useDisclosure } from "@mantine/hooks"; import theme from "./theme.js"; import logoUrl from "../assets/logo.svg"; import { useStore } from "../state.js"; -import { useEffect } from "react"; -import { trpc } from "../trpc/client.js"; +import { useEffect, useState } from "react"; +import { TRPCProvider, useTRPC } from "../trpc/client.js"; import { usePageContext } from "vike-react/usePageContext"; import "./hover.css"; +import { + QueryClient, + QueryClientProvider, + useMutation, + useQuery, +} from "@tanstack/react-query"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import type { AppRouter } from "../trpc/router.js"; + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); +} + +let browserQueryClient: QueryClient | undefined = undefined; +function getQueryClient() { + if (typeof window === "undefined") { + // Server: always make a new query client + return makeQueryClient(); + } + // Browser: make a new query client if we don't already have one + // This is very important, so we don't re-make a new client if React + // suspends during the initial render. This may not be needed if we + // have a suspense boundary BELOW the creation of the query client + if (!browserQueryClient) browserQueryClient = makeQueryClient(); + return browserQueryClient; +} export default function LayoutDefault({ children, -}: { children: React.ReactNode }) { +}: { + children: React.ReactNode; +}) { const pageContext = usePageContext(); const { urlPathname } = pageContext; const [opened, { toggle }] = useDisclosure(); - const conversations = useStore((state) => state.conversations); - const setConversations = useStore((state) => state.setConversations); + + const queryClient = getQueryClient(); + const [trpc] = useState(() => + createTRPCClient({ + links: [ + httpBatchLink({ + url: "/api/trpc", + methodOverride: "POST", + }), + ], + }) + ); + + return ( + + + + + + + + + {" "} + {" "} + + + + + + + + + + {children} + + + + + ); +} + +function NavLinkChat() { + const pageContext = usePageContext(); + const { urlPathname } = pageContext; + const trpc = useTRPC(); + // const + const startConversation = useMutation( + trpc.chat.conversations.start.mutationOptions() + ); + const deleteConversation = useMutation( + trpc.chat.conversations.deleteOne.mutationOptions() + ); + const { data: conversations } = useQuery( + trpc.chat.conversations.fetchAll.queryOptions() + ); + // TODO: should we be using zustand for this, or trpc/react-query's useMutation? const addConversation = useStore((state) => state.addConversation); const removeConversation = useStore((state) => state.removeConversation); const conversationId = useStore((state) => state.selectedConversationId); - useEffect(() => { - trpc.chat.conversations.fetchAll.query().then((res) => { - setConversations(res); - }); - }, [setConversations]); - - // useEffect(() => { - // if (isConversationListExpanded) { - // trpc.chat.listConversations.query().then((res) => { - // setConversations(res); - // }); - // } - // }, [isConversationListExpanded]); - async function handleDeleteConversation(conversationId: string) { removeConversation(conversationId); - await trpc.chat.conversations.deleteOne.mutate({ id: conversationId }); - const res = await trpc.chat.conversations.start.mutate(); + await deleteConversation.mutateAsync({ id: conversationId }); + const res = await startConversation.mutateAsync(); if (!res?.id) return; addConversation(res); await navigate(`/chat/${res.id}`); } return ( - - - - - - - {" "} - {" "} - - - - - - - + Chats + { + e.preventDefault(); + e.stopPropagation(); + startConversation.mutateAsync().then((res) => { + if (!res?.id) return; + addConversation(res); + navigate(`/chat/${res.id}`); + }); + }} /> - - Chats - { - e.preventDefault(); - e.stopPropagation(); - trpc.chat.conversations.start.mutate().then((res) => { - if (!res?.id) return; - addConversation(res); - navigate(`/chat/${res.id}`); - }); - }} - /> - - } - leftSection={} - rightSection={ - + } + leftSection={} + rightSection={ + + } + variant="subtle" + active={urlPathname.startsWith("/chat")} + defaultOpened={true} + > + {conversations?.map((conversation) => ( + + + - } - variant="subtle" - active={urlPathname.startsWith("/chat")} - defaultOpened={true} - > - {conversations.map((conversation) => ( - - - - - } - rightSection={ - <> - { - e.stopPropagation(); - e.preventDefault(); - handleDeleteConversation(conversation.id); - }} - className="show-by-default" - /> - { - e.stopPropagation(); - e.preventDefault(); - handleDeleteConversation(conversation.id); - }} - className="show-on-hover border-on-hover" - /> - - } - variant="subtle" - active={conversation.id === conversationId} + + } + rightSection={ + <> + { + e.stopPropagation(); + e.preventDefault(); + handleDeleteConversation(conversation.id); + }} + className="show-by-default" + /> + { + e.stopPropagation(); + e.preventDefault(); + handleDeleteConversation(conversation.id); + }} + className="show-on-hover border-on-hover" /> - ))} - - - {children} - - + + } + variant="subtle" + active={conversation.id === conversationId} + /> + ))} + ); } diff --git a/package.json b/package.json index d72f869..88d99de 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "@openrouter/ai-sdk-provider": "^1.1.2", "@sinclair/typebox": "^0.34.37", "@tabler/icons-react": "^3.34.1", - "@trpc/client": "^11.4.2", - "@trpc/server": "^11.4.2", + "@tanstack/react-query": "^5.85.3", + "@trpc/client": "^11.4.4", + "@trpc/server": "^11.4.4", + "@trpc/tanstack-react-query": "^11.4.4", "@universal-middleware/core": "^0.4.8", "@universal-middleware/hono": "^0.4.14", "@vitejs/plugin-react": "^4.6.0", diff --git a/pages/chat/+Page.tsx b/pages/chat/+Page.tsx new file mode 100644 index 0000000..6a3f756 --- /dev/null +++ b/pages/chat/+Page.tsx @@ -0,0 +1,41 @@ +import { Card, Textarea } from "@mantine/core"; +import { useState } from "react"; +import { useTRPC } from "../../trpc/client"; + +export default function ChatPage() { + const [inputMessage, setInputMessage] = useState(""); + const [loading, setLoading] = useState(false); + const [outputMessage, setOutputMessage] = useState(""); + const trpc = useTRPC(); + + async function handleSendMessage() { + setLoading(true); + const response = await trpc.chat.streamMessage.subscribe( + { + message: inputMessage, + }, + {} + ); + for await (const chunk of response) { + setOutputMessage(chunk); + } + setLoading(false); + } + return ( +
+ {outputMessage} +