Compare commits

...

10 Commits

@ -68,9 +68,18 @@ function getQueryClient() {
// suspends during the initial render. This may not be needed if we // suspends during the initial render. This may not be needed if we
// have a suspense boundary BELOW the creation of the query client // have a suspense boundary BELOW the creation of the query client
if (!browserQueryClient) browserQueryClient = makeQueryClient(); if (!browserQueryClient) browserQueryClient = makeQueryClient();
// This code is for all users
window.__TANSTACK_QUERY_CLIENT__ = browserQueryClient;
return browserQueryClient; return browserQueryClient;
} }
// This code is only for TypeScript
declare global {
interface Window {
__TANSTACK_QUERY_CLIENT__: import("@tanstack/query-core").QueryClient;
}
}
export function SignInWithGoogle() { export function SignInWithGoogle() {
const pageContext = usePageContext(); const pageContext = usePageContext();
/** This is populated using the +onCreatePageContext.server.ts hook */ /** This is populated using the +onCreatePageContext.server.ts hook */
@ -232,7 +241,9 @@ function NavLinkChat() {
const { urlPathname } = pageContext; const { urlPathname } = pageContext;
const trpc = useTRPC(); const trpc = useTRPC();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
// const const { data: conversations } = useQuery(
trpc.chat.conversations.fetchAll.queryOptions()
);
const startConversation = useMutation( const startConversation = useMutation(
trpc.chat.conversations.start.mutationOptions({ trpc.chat.conversations.start.mutationOptions({
onSuccess: () => { onSuccess: () => {
@ -286,9 +297,6 @@ function NavLinkChat() {
}) })
); );
const { data: conversations } = useQuery(
trpc.chat.conversations.fetchAll.queryOptions()
);
// const selectedConversationId = useStore( // const selectedConversationId = useStore(
// (state) => state.selectedConversationId // (state) => state.selectedConversationId
// ); // );
@ -356,7 +364,7 @@ function NavLinkChat() {
> >
{conversations?.map((conversation) => ( {conversations?.map((conversation) => (
<NavLink <NavLink
key={conversation.id} key={conversation.id + conversation.title}
href={`/chat/${conversation.id}`} href={`/chat/${conversation.id}`}
label={conversation.title} label={conversation.title}
className="hover-container" className="hover-container"

@ -19,7 +19,6 @@
"@mantine/hooks": "^8.1.1", "@mantine/hooks": "^8.1.1",
"@neondatabase/serverless": "^1.0.1", "@neondatabase/serverless": "^1.0.1",
"@openrouter/ai-sdk-provider": "^1.1.2", "@openrouter/ai-sdk-provider": "^1.1.2",
"@sinclair/typebox": "^0.34.37",
"@tabler/icons-react": "^3.34.1", "@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.85.3", "@tanstack/react-query": "^5.85.3",
"@trpc/client": "^11.4.4", "@trpc/client": "^11.4.4",

@ -316,14 +316,18 @@ export default function ChatPage() {
newConversations: null, newConversations: null,
}; };
} }
const newConversations: Array<Conversation> = [
...previousConversations, /** MUST make a deep, immutable copy in order to trigger re-render of
{ * oberserving components. */
...conversation, const newConversations: Array<Conversation> = JSON.parse(
title, JSON.stringify(previousConversations)
} as Conversation, );
]; const conversationToUpdate = newConversations.find((c) => c.id === id);
queryClient.setQueryData( if (!conversationToUpdate) {
return { previousConversations, newConversations };
}
conversationToUpdate.title = title;
queryClient.setQueryData<Array<Conversation>>(
trpc.chat.conversations.fetchAll.queryKey(), trpc.chat.conversations.fetchAll.queryKey(),
newConversations newConversations
); );
@ -505,6 +509,12 @@ export default function ChatPage() {
// onChange={(e) => { // onChange={(e) => {
// setConversationTitle(e.target.value); // setConversationTitle(e.target.value);
// }} // }}
onKeyUp={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.currentTarget.blur();
}
}}
onBlur={(e) => { onBlur={(e) => {
updateConversationTitle.mutateAsync({ updateConversationTitle.mutateAsync({
id: conversationId, id: conversationId,

@ -1,7 +1,7 @@
import type { PageContextServer } from "vike/types"; import type { PageContextServer } from "vike/types";
import { createCaller } from "../../../server/trpc/chat.js"; import { createCaller } from "../../../server/trpc/chat.js";
import { getDbClient } from "../../../database/postgres.js"; import { getDbClient } from "../../../database/postgres.js";
import { getOpenrouter } from "../../../server/provider.js"; import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { env } from "../../../server/env.js"; import { env } from "../../../server/env.js";
export type Data = Awaited<ReturnType<typeof data>>; export type Data = Awaited<ReturnType<typeof data>>;
@ -9,9 +9,10 @@ export type Data = Awaited<ReturnType<typeof data>>;
export const data = async (pageContext: PageContextServer) => { export const data = async (pageContext: PageContextServer) => {
const { id } = pageContext.routeParams; const { id } = pageContext.routeParams;
const caller = createCaller({ const caller = createCaller({
openrouter: getOpenrouter( openrouter: createOpenRouter({
(pageContext.env?.OPENROUTER_API_KEY || env.OPENROUTER_API_KEY) as string apiKey: (pageContext.env?.OPENROUTER_API_KEY ||
), env.OPENROUTER_API_KEY) as string,
}),
jwt: pageContext.session?.jwt, jwt: pageContext.session?.jwt,
dbClient: getDbClient( dbClient: getDbClient(
(pageContext.env?.POSTGRES_CONNECTION_STRING || (pageContext.env?.POSTGRES_CONNECTION_STRING ||

@ -32,9 +32,6 @@ importers:
'@openrouter/ai-sdk-provider': '@openrouter/ai-sdk-provider':
specifier: ^1.1.2 specifier: ^1.1.2
version: 1.1.2(ai@5.0.9(zod@4.0.17))(zod@4.0.17) version: 1.1.2(ai@5.0.9(zod@4.0.17))(zod@4.0.17)
'@sinclair/typebox':
specifier: ^0.34.37
version: 0.34.37
'@tabler/icons-react': '@tabler/icons-react':
specifier: ^3.34.1 specifier: ^3.34.1
version: 3.34.1(react@19.1.0) version: 3.34.1(react@19.1.0)
@ -1155,9 +1152,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@sinclair/typebox@0.34.37':
resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==}
'@standard-schema/spec@1.0.0': '@standard-schema/spec@1.0.0':
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
@ -3936,8 +3930,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.44.1': '@rollup/rollup-win32-x64-msvc@4.44.1':
optional: true optional: true
'@sinclair/typebox@0.34.37': {}
'@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.0.0': {}
'@tabler/icons-react@3.34.1(react@19.1.0)': '@tabler/icons-react@3.34.1(react@19.1.0)':

@ -8,17 +8,13 @@ import {
import GoogleProvider from "@auth/core/providers/google"; import GoogleProvider from "@auth/core/providers/google";
import type { Session } from "@auth/core/types"; import type { Session } from "@auth/core/types";
// TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.) // TODO: stop using universal-middleware and directly integrate server middlewares instead and/or use vike-server https://vike.dev/server. (Bati generates boilerplates that use universal-middleware https://github.com/magne4000/universal-middleware to make Bati's internal logic easier. This is temporary and will be removed soon.)
import type { import {
Get, type Get,
UniversalHandler, type UniversalHandler,
UniversalMiddleware, type UniversalMiddleware,
env as getEnv,
} from "@universal-middleware/core"; } from "@universal-middleware/core";
import { env } from "./env.js";
import { getDbClient } from "../database/index.js"; import { getDbClient } from "../database/index.js";
import { JWT } from "@auth/core/jwt";
const POSTGRES_CONNECTION_STRING =
"postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true";
if (!globalThis.crypto) { if (!globalThis.crypto) {
/** /**
@ -33,111 +29,114 @@ if (!globalThis.crypto) {
}); });
} }
const authjsConfig = { const authjsConfig = (env: Record<string, string>) =>
basePath: "/api/auth", ({
trustHost: Boolean( basePath: "/api/auth",
env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production" // trustHost: Boolean(
), // env.AUTH_TRUST_HOST ?? env.VERCEL ?? env.NODE_ENV !== "production"
// TODO: Replace secret {@see https://authjs.dev/reference/core#secret} // ),
secret: "buginoo", trustHost: true,
providers: [ // TODO: Replace secret {@see https://authjs.dev/reference/core#secret}
// TODO: Choose and implement providers secret: env.AUTHJS_SECRET,
// CredentialsProvider({ providers: [
// name: "Credentials", // TODO: Choose and implement providers
// credentials: { // CredentialsProvider({
// username: { label: "Username", type: "text", placeholder: "jsmith" }, // name: "Credentials",
// password: { label: "Password", type: "password" }, // credentials: {
// }, // username: { label: "Username", type: "text", placeholder: "jsmith" },
// async authorize() { // password: { label: "Password", type: "password" },
// // Add logic here to look up the user from the credentials supplied // },
// const user = { // async authorize() {
// id: "019900bb-61b3-7333-b760-b27784dfe33b", // // Add logic here to look up the user from the credentials supplied
// name: "J Smith", // const user = {
// email: "jsmith@example.com", // id: "019900bb-61b3-7333-b760-b27784dfe33b",
// }; // name: "J Smith",
// email: "jsmith@example.com",
// };
// // Any object returned will be saved in `user` property of the JWT // // Any object returned will be saved in `user` property of the JWT
// // If you return null then an error will be displayed advising the user to check their details. // // If you return null then an error will be displayed advising the user to check their details.
// // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter // // You can also Reject this callback with an Error thus the user will be sent to the error page with the error message as a query parameter
// return user ?? null; // return user ?? null;
// }, // },
// }), // }),
GoogleProvider({ GoogleProvider({
clientId: clientId: env.GOOGLE_CLIENT_ID,
"697711350664-t6237s5n3ttjd1npp1qif1aupptkr0va.apps.googleusercontent.com", clientSecret: env.GOOGLE_CLIENT_SECRET,
clientSecret: "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", }),
}), ],
], callbacks: {
callbacks: { async signIn({ user, account, profile }) {
async signIn({ user, account, profile }) { if (typeof user?.email !== "string") return false;
if (typeof user?.email !== "string") return false; //@ts-ignore
const dbClient = await getDbClient(POSTGRES_CONNECTION_STRING); const dbClient = await getDbClient(env.POSTGRES_CONNECTION_STRING);
let userFromDb = await dbClient let userFromDb = await dbClient
.selectFrom("users") .selectFrom("users")
.selectAll() .selectAll()
.where("email", "=", user.email) .where("email", "=", user.email)
.executeTakeFirst(); .executeTakeFirst();
if (!userFromDb) { if (!userFromDb) {
userFromDb = ( userFromDb = (
await dbClient await dbClient
.insertInto("users") .insertInto("users")
.values({ .values({
email: user.email, email: user.email,
username: user.email, username: user.email,
password: null, password: null,
createdAt: null, createdAt: null,
lastLogin: null, lastLogin: null,
}) })
.returningAll() .returningAll()
.execute() .execute()
)[0]; )[0];
} }
console.log("signIn", user, account, profile); console.log("signIn", user, account, profile);
return true; return true;
}, },
jwt: async ({ token }) => { jwt: async ({ token }) => {
if (typeof token?.email !== "string") return token; if (typeof token?.email !== "string") return token;
const dbClient = await getDbClient(POSTGRES_CONNECTION_STRING); //@ts-ignore
let userFromDb = await dbClient const dbClient = await getDbClient(env.POSTGRES_CONNECTION_STRING);
.selectFrom("users") let userFromDb = await dbClient
.selectAll() .selectFrom("users")
.where("email", "=", token.email || "") .selectAll()
.executeTakeFirst(); .where("email", "=", token.email || "")
/** TODO: the following should never happen, because the account in .executeTakeFirst();
* created in the `isgnIn` step; but I don't know what error to throw here /** TODO: the following should never happen, because the account in
* if for some reason there is no such account.*/ * created in the `isgnIn` step; but I don't know what error to throw here
if (!userFromDb) { * if for some reason there is no such account.*/
userFromDb = ( if (!userFromDb) {
await dbClient userFromDb = (
.insertInto("users") await dbClient
.values({ .insertInto("users")
email: token.email, .values({
username: token.email, email: token.email,
password: null, username: token.email,
createdAt: null, password: null,
lastLogin: null, createdAt: null,
}) lastLogin: null,
.returningAll() })
.execute() .returningAll()
)[0]; .execute()
} )[0];
return { }
...token, return {
id: userFromDb?.id || "", ...token,
}; id: userFromDb?.id || "",
}, };
session: ({ token, session }) => { },
return { session: ({ token, session }) => {
...session, return {
user: { ...session,
...session.user, user: {
id: token.id as string, ...session.user,
}, id: token.id as string,
jwt: token, },
}; jwt: token,
};
},
}, },
}, } satisfies Omit<AuthConfig, "raw">);
} satisfies Omit<AuthConfig, "raw">;
/** /**
* Retrieve Auth.js session from Request * Retrieve Auth.js session from Request
@ -175,11 +174,15 @@ export async function getSession(
* @link {@see https://authjs.dev/getting-started/session-management/get-session} * @link {@see https://authjs.dev/getting-started/session-management/get-session}
**/ **/
export const authjsSessionMiddleware: Get<[], UniversalMiddleware> = export const authjsSessionMiddleware: Get<[], UniversalMiddleware> =
() => async (request, context) => { () => async (request, context, runtime) => {
const env = getEnv(runtime);
try { try {
return { return {
...context, ...context,
session: await getSession(request, authjsConfig), session: await getSession(
request,
authjsConfig(env as Record<string, string>)
),
}; };
} catch (error) { } catch (error) {
console.debug("authjsSessionMiddleware:", error); console.debug("authjsSessionMiddleware:", error);
@ -194,6 +197,7 @@ export const authjsSessionMiddleware: Get<[], UniversalMiddleware> =
* Auth.js route * Auth.js route
* @link {@see https://authjs.dev/getting-started/installation} * @link {@see https://authjs.dev/getting-started/installation}
**/ **/
export const authjsHandler = (() => async (request) => { export const authjsHandler = (() => async (request, context, runtime) => {
return Auth(request, authjsConfig); const env = getEnv(runtime);
return Auth(request, authjsConfig(env as Record<string, string>));
}) satisfies Get<[], UniversalHandler>; }) satisfies Get<[], UniversalHandler>;

@ -1,14 +1,3 @@
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { env } from "./env.js";
export const openrouter = createOpenRouter({
apiKey: env.OPENROUTER_API_KEY,
});
export function getOpenrouter(OPENROUTER_API_KEY: string) {
return createOpenRouter({
apiKey: OPENROUTER_API_KEY,
});
}
export const MODEL_NAME = "mistralai/mistral-nemo"; export const MODEL_NAME = "mistralai/mistral-nemo";
// export const MODEL_NAME = "z-ai/glm-4.5-air"; // export const MODEL_NAME = "z-ai/glm-4.5-air";
// export const MODEL_NAME = "openai/gpt-5-mini"; // export const MODEL_NAME = "openai/gpt-5-mini";

@ -7,7 +7,7 @@ import {
} from "@universal-middleware/core"; } from "@universal-middleware/core";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { getDbClient } from "../database/postgres"; import { getDbClient } from "../database/postgres";
import { getOpenrouter } from "./provider.js"; import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { env as processEnv } from "./env.js"; import { env as processEnv } from "./env.js";
import { getToken } from "@auth/core/jwt"; import { getToken } from "@auth/core/jwt";
@ -22,10 +22,22 @@ export const trpcHandler = ((endpoint) => (request, context, runtime) => {
(env.POSTGRES_CONNECTION_STRING || (env.POSTGRES_CONNECTION_STRING ||
processEnv.POSTGRES_CONNECTION_STRING) as string processEnv.POSTGRES_CONNECTION_STRING) as string
); );
const openrouter = getOpenrouter( const openrouter = createOpenRouter({
(env.OPENROUTER_API_KEY || processEnv.OPENROUTER_API_KEY) as string apiKey: (env.OPENROUTER_API_KEY ||
); processEnv.OPENROUTER_API_KEY) as string,
const jwt = await getToken({ req: request, secret: "buginoo" }); });
const jwt = await getToken({
req,
secret: (env.AUTHJS_SECRET || processEnv.AUTHJS_SECRET) as string,
/** Needed to specify cookie name because for some reason in production
* it wasn't reading the correct cookie but in development it was. It
* was not straightforward to fix the name of the cookie in
* `authOptions`, so I adjusted what cookie to read from here: */
cookieName:
env.NODE_ENV === "production"
? "__Secure-authjs.session-token"
: "authjs.session-token",
});
return { return {
...context, ...context,
...runtime, ...runtime,

@ -1,6 +1,6 @@
import { router, createCallerFactory, authProcedure } from "./server.js"; import { router, createCallerFactory, authProcedure } from "./server.js";
import type { DraftMessage } from "../../types.js"; import type { DraftMessage } from "../../types.js";
import { MODEL_NAME, openrouter } from "../provider.js"; import { MODEL_NAME } from "../provider.js";
import { generateObject, generateText, jsonSchema } from "ai"; import { generateObject, generateText, jsonSchema } from "ai";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";

@ -1,8 +1,6 @@
import type { TSchema } from "@sinclair/typebox";
import { TypeCompiler } from "@sinclair/typebox/compiler";
import { initTRPC, TRPCError } from "@trpc/server"; import { initTRPC, TRPCError } from "@trpc/server";
import type { getDbClient } from "../../database/postgres"; import type { getDbClient } from "../../database/postgres";
import type { getOpenrouter } from "@server/provider.js"; import type { createOpenRouter } from "@openrouter/ai-sdk-provider";
import type { JWT } from "@auth/core/jwt"; import type { JWT } from "@auth/core/jwt";
/** /**
@ -13,7 +11,7 @@ const t = initTRPC
.context< .context<
object & { object & {
dbClient: ReturnType<typeof getDbClient>; dbClient: ReturnType<typeof getDbClient>;
openrouter: ReturnType<typeof getOpenrouter>; openrouter: ReturnType<typeof createOpenRouter>;
jwt?: JWT | null; jwt?: JWT | null;
} }
>() >()
@ -53,22 +51,4 @@ export const authProcedure = publicProcedure.use(
} }
); );
/**
* Generate a TRPC-compatible validator function given a Typebox schema.
* This was copied from [https://github.com/sinclairzx81/typebox/blob/6cfcdc02cc813af2f1be57407c771fc4fadfc34a/example/trpc/readme.md].
* @param schema A Typebox schema
* @returns A TRPC-compatible validator function
*/
export function Validator<T extends TSchema>(schema: T) {
const check = TypeCompiler.Compile(schema);
return (value: unknown) => {
if (check.Check(value)) return value;
const err = check.Errors(value).First();
throw new TRPCError({
message: `${err?.message} for ${err?.path}`,
code: "BAD_REQUEST",
});
};
}
export const createCallerFactory = t.createCallerFactory; export const createCallerFactory = t.createCallerFactory;

@ -6,3 +6,14 @@ compatibility_flags = [ "nodejs_compat" ]
[vars] [vars]
MILVUS_ADDRESS = "in03-639fdba4bcde098.serverless.gcp-us-west1.cloud.zilliz.com" MILVUS_ADDRESS = "in03-639fdba4bcde098.serverless.gcp-us-west1.cloud.zilliz.com"
MILVUS_USERNAME = "db_639fdba4bcde098" MILVUS_USERNAME = "db_639fdba4bcde098"
[env.production.vars]
MILVUS_ADDRESS = "in03-639fdba4bcde098.serverless.gcp-us-west1.cloud.zilliz.com"
MILVUS_USERNAME = "db_639fdba4bcde098"
GOOGLE_CLIENT_ID = "697711350664-t6237s5n3ttjd1npp1qif1aupptkr0va.apps.googleusercontent.com"
NODE_ENV = "production"
[env.preview.vars]
MILVUS_ADDRESS = "in03-639fdba4bcde098.serverless.gcp-us-west1.cloud.zilliz.com"
MILVUS_USERNAME = "db_639fdba4bcde098"
NODE_ENV = "development"
Loading…
Cancel
Save