move app to app/ subdir

This commit is contained in:
Kyle Corbitt
2023-08-05 10:00:10 -07:00
parent 7707d451e0
commit 21ef67ed4c
203 changed files with 0 additions and 0 deletions

View 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();
};

View 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;
};

View 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
View 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>;

View 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
View 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
View 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) ?? [];

View 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
View 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}`;

View 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);