move app to app/ subdir
This commit is contained in:
87
app/src/utils/accessControl.ts
Normal file
87
app/src/utils/accessControl.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { OrganizationUserRole } from "@prisma/client";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { type TRPCContext } from "~/server/api/trpc";
|
||||
import { prisma } from "~/server/db";
|
||||
|
||||
const isAdmin = async (userId: string) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: userId, role: "ADMIN" },
|
||||
});
|
||||
|
||||
return !!user;
|
||||
};
|
||||
|
||||
// No-op method for protected routes that really should be accessible to anyone.
|
||||
export const requireNothing = (ctx: TRPCContext) => {
|
||||
ctx.markAccessControlRun();
|
||||
};
|
||||
|
||||
export const requireCanViewDataset = async (datasetId: string, ctx: TRPCContext) => {
|
||||
const dataset = await prisma.dataset.findFirst({
|
||||
where: {
|
||||
id: datasetId,
|
||||
organization: {
|
||||
organizationUsers: {
|
||||
some: {
|
||||
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
|
||||
userId: ctx.session?.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!dataset) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
ctx.markAccessControlRun();
|
||||
};
|
||||
|
||||
export const requireCanModifyDataset = async (datasetId: string, ctx: TRPCContext) => {
|
||||
// Right now all users who can view a dataset can also modify it
|
||||
await requireCanViewDataset(datasetId, ctx);
|
||||
};
|
||||
|
||||
export const requireCanViewExperiment = async (experimentId: string, ctx: TRPCContext) => {
|
||||
await prisma.experiment.findFirst({
|
||||
where: { id: experimentId },
|
||||
});
|
||||
|
||||
// Right now all experiments are publicly viewable, so this is a no-op.
|
||||
ctx.markAccessControlRun();
|
||||
};
|
||||
|
||||
export const canModifyExperiment = async (experimentId: string, userId: string) => {
|
||||
const [adminUser, experiment] = await Promise.all([
|
||||
isAdmin(userId),
|
||||
prisma.experiment.findFirst({
|
||||
where: {
|
||||
id: experimentId,
|
||||
organization: {
|
||||
organizationUsers: {
|
||||
some: {
|
||||
role: { in: [OrganizationUserRole.ADMIN, OrganizationUserRole.MEMBER] },
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return adminUser || !!experiment;
|
||||
};
|
||||
|
||||
export const requireCanModifyExperiment = async (experimentId: string, ctx: TRPCContext) => {
|
||||
const userId = ctx.session?.user.id;
|
||||
if (!userId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
if (!(await canModifyExperiment(experimentId, userId))) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
ctx.markAccessControlRun();
|
||||
};
|
||||
31
app/src/utils/analytics/clientAnalytics.ts
Normal file
31
app/src/utils/analytics/clientAnalytics.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { type Session } from "next-auth";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import posthog from "posthog-js";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
// Make sure we're in the browser
|
||||
const enableBrowserAnalytics = typeof window !== "undefined";
|
||||
|
||||
if (env.NEXT_PUBLIC_POSTHOG_KEY && enableBrowserAnalytics) {
|
||||
posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
api_host: `${env.NEXT_PUBLIC_HOST}/ingest`,
|
||||
});
|
||||
}
|
||||
|
||||
export const identifySession = (session: Session) => {
|
||||
if (!session.user) return;
|
||||
posthog.identify(session.user.id, {
|
||||
name: session.user.name,
|
||||
email: session.user.email,
|
||||
});
|
||||
};
|
||||
|
||||
export const SessionIdentifier = () => {
|
||||
const session = useSession().data;
|
||||
useEffect(() => {
|
||||
if (session && enableBrowserAnalytics) identifySession(session);
|
||||
}, [session]);
|
||||
return null;
|
||||
};
|
||||
14
app/src/utils/analytics/serverAnalytics.ts
Normal file
14
app/src/utils/analytics/serverAnalytics.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type Session } from "next-auth";
|
||||
import { PostHog } from "posthog-node";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const posthogServerClient = env.NEXT_PUBLIC_POSTHOG_KEY
|
||||
? new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: "https://app.posthog.com",
|
||||
})
|
||||
: null;
|
||||
|
||||
export const capturePath = (session: Session, path: string) => {
|
||||
if (!session.user || !posthogServerClient) return;
|
||||
posthogServerClient?.capture({ distinctId: session.user.id, event: path });
|
||||
};
|
||||
69
app/src/utils/api.ts
Normal file
69
app/src/utils/api.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
|
||||
* contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
|
||||
*
|
||||
* We also create a few inference helpers for input and output types.
|
||||
*/
|
||||
import { httpBatchLink, loggerLink } from "@trpc/client";
|
||||
import { createTRPCNext } from "@trpc/next";
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
import { type AppRouter } from "~/server/api/root.router";
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||
};
|
||||
|
||||
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||
export const api = createTRPCNext<AppRouter>({
|
||||
config() {
|
||||
return {
|
||||
/**
|
||||
* Transformer used for data de-serialization from the server.
|
||||
*
|
||||
* @see https://trpc.io/docs/data-transformers
|
||||
*/
|
||||
transformer: superjson,
|
||||
|
||||
/**
|
||||
* Links used to determine request flow from client to server.
|
||||
*
|
||||
* @see https://trpc.io/docs/links
|
||||
*/
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
(process.env.NODE_ENV ?? "development") === "development" ||
|
||||
(opts.direction === "down" && opts.result instanceof Error),
|
||||
}),
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
}),
|
||||
],
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Whether tRPC should await queries when server rendering pages.
|
||||
*
|
||||
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
||||
*/
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Inference helper for inputs.
|
||||
*
|
||||
* @example type HelloInput = RouterInputs['example']['hello']
|
||||
*/
|
||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
|
||||
/**
|
||||
* Inference helper for outputs.
|
||||
*
|
||||
* @example type HelloOutput = RouterOutputs['example']['hello']
|
||||
*/
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
|
||||
export type APIClient = ReturnType<typeof api.useContext>;
|
||||
17
app/src/utils/countTokens.ts
Normal file
17
app/src/utils/countTokens.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type ChatCompletion } from "openai/resources/chat";
|
||||
import { GPTTokens } from "gpt-tokens";
|
||||
import { type SupportedModel } from "~/modelProviders/openai-ChatCompletion";
|
||||
|
||||
interface GPTTokensMessageItem {
|
||||
name?: string;
|
||||
role: "system" | "user" | "assistant";
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const countOpenAIChatTokens = (
|
||||
model: SupportedModel,
|
||||
messages: ChatCompletion.Choice.Message[],
|
||||
) => {
|
||||
return new GPTTokens({ model, messages: messages as unknown as GPTTokensMessageItem[] })
|
||||
.usedTokens;
|
||||
};
|
||||
11
app/src/utils/dayjs.ts
Normal file
11
app/src/utils/dayjs.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
export const formatTimePast = (date: Date) =>
|
||||
dayjs.duration(dayjs(date).diff(dayjs())).humanize(true);
|
||||
|
||||
export default dayjs;
|
||||
134
app/src/utils/hooks.ts
Normal file
134
app/src/utils/hooks.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { type RefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { NumberParam, useQueryParam, withDefault } from "use-query-params";
|
||||
|
||||
export const useExperiment = () => {
|
||||
const router = useRouter();
|
||||
const experiment = api.experiments.get.useQuery(
|
||||
{ id: router.query.id as string },
|
||||
{ enabled: !!router.query.id },
|
||||
);
|
||||
|
||||
return experiment;
|
||||
};
|
||||
|
||||
export const useExperimentAccess = () => {
|
||||
return useExperiment().data?.access ?? { canView: false, canModify: false };
|
||||
};
|
||||
|
||||
export const useDataset = () => {
|
||||
const router = useRouter();
|
||||
const dataset = api.datasets.get.useQuery(
|
||||
{ id: router.query.id as string },
|
||||
{ enabled: !!router.query.id },
|
||||
);
|
||||
|
||||
return dataset;
|
||||
};
|
||||
|
||||
export const useDatasetEntries = () => {
|
||||
const dataset = useDataset();
|
||||
const [page] = usePage();
|
||||
|
||||
return api.datasetEntries.list.useQuery(
|
||||
{ datasetId: dataset.data?.id ?? "", page },
|
||||
{ enabled: dataset.data?.id != null },
|
||||
);
|
||||
};
|
||||
|
||||
type AsyncFunction<T extends unknown[], U> = (...args: T) => Promise<U>;
|
||||
|
||||
export function useHandledAsyncCallback<T extends unknown[], U>(
|
||||
callback: AsyncFunction<T, U>,
|
||||
deps: React.DependencyList,
|
||||
) {
|
||||
const [loading, setLoading] = useState(0);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
const wrappedCallback = useCallback((...args: T) => {
|
||||
setLoading((loading) => loading + 1);
|
||||
setError(null);
|
||||
|
||||
callback(...args)
|
||||
.catch((error) => {
|
||||
setError(error as Error);
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading((loading) => loading - 1);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
|
||||
return [wrappedCallback, loading > 0, error] as const;
|
||||
}
|
||||
|
||||
// Have to do this ugly thing to convince Next not to try to access `navigator`
|
||||
// on the server side at build time, when it isn't defined.
|
||||
export const useModifierKeyLabel = () => {
|
||||
const [label, setLabel] = useState("");
|
||||
useEffect(() => {
|
||||
setLabel(navigator?.platform?.startsWith("Mac") ? "⌘" : "Ctrl");
|
||||
}, []);
|
||||
return label;
|
||||
};
|
||||
|
||||
interface Dimensions {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
// get dimensions of an element
|
||||
export const useElementDimensions = (): [RefObject<HTMLElement>, Dimensions | undefined] => {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [dimensions, setDimensions] = useState<Dimensions | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
setDimensions(entry.contentRect);
|
||||
});
|
||||
});
|
||||
|
||||
const observedRef = ref.current;
|
||||
|
||||
observer.observe(observedRef);
|
||||
|
||||
// Cleanup the observer on component unmount
|
||||
return () => {
|
||||
if (observedRef) {
|
||||
observer.unobserve(observedRef);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [ref, dimensions];
|
||||
};
|
||||
|
||||
export const usePage = () => useQueryParam("page", withDefault(NumberParam, 1));
|
||||
|
||||
export const useScenarios = () => {
|
||||
const experiment = useExperiment();
|
||||
const [page] = usePage();
|
||||
|
||||
return api.scenarios.list.useQuery(
|
||||
{ experimentId: experiment.data?.id ?? "", page },
|
||||
{ enabled: experiment.data?.id != null },
|
||||
);
|
||||
};
|
||||
|
||||
export const useScenario = (scenarioId: string) => {
|
||||
return api.scenarios.get.useQuery({ id: scenarioId });
|
||||
};
|
||||
|
||||
export const useVisibleScenarioIds = () => useScenarios().data?.scenarios.map((s) => s.id) ?? [];
|
||||
42
app/src/utils/useSocket.ts
Normal file
42
app/src/utils/useSocket.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
const url = env.NEXT_PUBLIC_SOCKET_URL;
|
||||
|
||||
export default function useSocket<T>(channel?: string | null) {
|
||||
const socketRef = useRef<Socket>();
|
||||
const [message, setMessage] = useState<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!channel) return;
|
||||
|
||||
console.log("connecting to channel", channel);
|
||||
// Create websocket connection
|
||||
socketRef.current = io(url);
|
||||
|
||||
socketRef.current.on("connect", () => {
|
||||
// Join the specific room
|
||||
socketRef.current?.emit("join", channel);
|
||||
|
||||
// Listen for 'message' events
|
||||
socketRef.current?.on("message", (message: T) => {
|
||||
setMessage(message);
|
||||
});
|
||||
});
|
||||
|
||||
// Unsubscribe and disconnect on cleanup
|
||||
return () => {
|
||||
if (socketRef.current) {
|
||||
if (channel) {
|
||||
socketRef.current.off("message");
|
||||
}
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = undefined;
|
||||
}
|
||||
setMessage(null);
|
||||
};
|
||||
}, [channel]);
|
||||
|
||||
return message;
|
||||
}
|
||||
12
app/src/utils/utils.ts
Normal file
12
app/src/utils/utils.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import frontendModelProviders from "~/modelProviders/frontendModelProviders";
|
||||
import { type ProviderModel } from "~/modelProviders/types";
|
||||
|
||||
export const truthyFilter = <T>(x: T | null | undefined): x is T => Boolean(x);
|
||||
|
||||
export const lookupModel = (provider: string, model: string) => {
|
||||
const modelObj = frontendModelProviders[provider as ProviderModel["provider"]]?.models[model];
|
||||
return modelObj ? { ...modelObj, provider } : null;
|
||||
};
|
||||
|
||||
export const modelLabel = (provider: string, model: string) =>
|
||||
`${provider}/${lookupModel(provider, model)?.name ?? model}`;
|
||||
4
app/src/utils/wsConnection.ts
Normal file
4
app/src/utils/wsConnection.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { io } from "socket.io-client";
|
||||
import { env } from "~/env.mjs";
|
||||
|
||||
export const wsConnection = io(env.NEXT_PUBLIC_SOCKET_URL);
|
||||
Reference in New Issue
Block a user