Skip to content

Commit

Permalink
Merge pull request #3454 from quantified-uncertainty/hub-optimizations
Browse files Browse the repository at this point in the history
Hub RSC rewrite
  • Loading branch information
berekuk authored Dec 5, 2024
2 parents c2aa55d + 215ca10 commit 1ddd150
Show file tree
Hide file tree
Showing 389 changed files with 8,030 additions and 17,188 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Turbo run
run: npx turbo run build test lint
run: npx turbo run build test lint --env-mode=loose
7 changes: 2 additions & 5 deletions packages/ai/src/scripts/fine-tuning-setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import * as fs from "fs";

import { examplesToImport } from "./fine-tuning/favoriteExamples.js";
import {
fetchCodeFromGraphQL,
fetchGroupModels,
} from "./squiggleHubHelpers.js";
import { fetchCodeFromHub, fetchGroupModels } from "./squiggleHubHelpers.js";

interface ProcessedModel {
prompt: string;
Expand Down Expand Up @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise<ProcessedModel[]> {
for (const example of examplesToImport) {
const [owner, slug] = example.id.split("/");
try {
const code = await fetchCodeFromGraphQL(owner, slug);
const code = await fetchCodeFromHub(owner, slug);
processedExamples.push({
prompt: example.prompt,
response: code.trim(),
Expand Down
7 changes: 2 additions & 5 deletions packages/ai/src/scripts/fine-tuning/setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import * as fs from "fs";

import {
fetchCodeFromGraphQL,
fetchGroupModels,
} from "../squiggleHubHelpers.js";
import { fetchCodeFromHub, fetchGroupModels } from "../squiggleHubHelpers.js";
import { examplesToImport } from "./favoriteExamples.js";

interface ProcessedModel {
Expand Down Expand Up @@ -98,7 +95,7 @@ async function processFavoriteExamples(): Promise<ProcessedModel[]> {
for (const example of examplesToImport) {
const [owner, slug] = example.id.split("/");
try {
const code = await fetchCodeFromGraphQL(owner, slug);
const code = await fetchCodeFromHub(owner, slug);
processedExamples.push({
prompt: example.prompt,
response: code.trim(),
Expand Down
98 changes: 25 additions & 73 deletions packages/ai/src/scripts/squiggleHubHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,36 @@
import axios from "axios";
import { z } from "zod";

export const librariesToImport = ["ozziegooen/sTest", "ozziegooen/helpers"];
const SERVER = "https://squigglehub.org";

const GRAPHQL_URL = "https://squigglehub.org/api/graphql";

export function getQuery(owner: string, slug: string) {
return `
query GetModelCode {
model(input: {owner: "${owner}", slug: "${slug}"}) {
... on Model {
id
currentRevision {
content {
... on SquiggleSnippet {
id
code
}
}
}
}
}
}
`;
}

export async function fetchCodeFromGraphQL(
export async function fetchCodeFromHub(
owner: string,
slug: string
): Promise<string> {
const query = getQuery(owner, slug);
const response = await axios.post(GRAPHQL_URL, { query });
return response.data.data.model.currentRevision.content.code;
}
const data = await fetch(
`${SERVER}/api/get-source?${new URLSearchParams({
owner,
slug,
})}`
).then((res) => res.json());
const parsed = z.object({ code: z.string() }).safeParse(data);
if (!parsed.success) {
throw new Error(`Failed to fetch source for ${owner}/${slug}`);
}

export function getGroupModelsQuery(groupSlug: string) {
return {
query: `
query GetGroupModels($groupSlug: String!) {
group(slug: $groupSlug) {
... on Group {
id
models(first: 50) {
edges {
node {
currentRevision {
content {
... on SquiggleSnippet {
code
}
}
}
}
}
}
}
}
}
`,
variables: { groupSlug },
};
return parsed.data.code;
}

export async function fetchGroupModels(groupSlug: string): Promise<string[]> {
try {
const query = getGroupModelsQuery(groupSlug);
const response = await axios.post(GRAPHQL_URL, query);

if (response.status === 200) {
const models = response.data.data.group.models.edges;
console.log(`Fetched ${models.length} models from group ${groupSlug}`);

return models.map(
(model: { node: { currentRevision: { content: { code: string } } } }) =>
model.node.currentRevision.content.code
);
} else {
throw new Error(`Error fetching group models: ${response.statusText}`);
}
} catch (error) {
console.error("Error fetching group models:", error);
throw error;
const data = await fetch(
`${SERVER}/api/get-group-models?${new URLSearchParams({ slug: groupSlug })}`
).then((res) => res.json());

const parsed = z
.object({ models: z.array(z.object({ slug: z.string() })) })
.safeParse(data);
if (!parsed.success) {
throw new Error(`Failed to fetch group models for ${groupSlug}`);
}

return parsed.data.models.map((item) => item.slug);
}
6 changes: 3 additions & 3 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"lodash": "^4.17.21",
"prettier": "^3.3.3",
"react-draggable": "^4.4.6",
"react-hook-form": "^7.50.0",
"react-hook-form": "^7.53.2",
"react-markdown": "^9.0.1",
"reactflow": "^11.11.4",
"remark-gfm": "^4.0.0",
Expand Down Expand Up @@ -82,8 +82,8 @@
"jsdom": "^25.0.1",
"postcss": "^8.4.38",
"postcss-cli": "^11.0.0",
"react": "^18.2.0",
"react-dom": "^18.3.1",
"react": "19.0.0-rc-66855b96-20241106",
"react-dom": "19.0.0-rc-66855b96-20241106",
"rollup-plugin-node-builtins": "^2.1.2",
"storybook": "^8.1.6",
"tailwindcss": "^3.4.3",
Expand Down
58 changes: 41 additions & 17 deletions packages/content/src/collections/squiggleAiLibraries.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
import { defineCollection } from "@content-collections/core";
import { z } from "zod";

const GRAPHQL_URL = "https://squigglehub.org/api/graphql";
const SERVER = "https://squigglehub.org";

export function getQuery(owner: string, slug: string) {
function getGraphqlQuery(owner: string, slug: string) {
return `
query GetModelCode {
model(input: {owner: "${owner}", slug: "${slug}"}) {
... on Model {
id
currentRevision {
content {
... on SquiggleSnippet {
id
code
query GetModelCode {
model(input: {owner: "${owner}", slug: "${slug}"}) {
... on Model {
id
currentRevision {
content {
... on SquiggleSnippet {
id
code
}
}
}
}
}
}
}
`;
`;
}

export async function fetchCodeFromGraphQL(
export async function fetchCodeFromHubLegacy(
owner: string,
slug: string
): Promise<string> {
const query = getQuery(owner, slug);
const response = await fetch(GRAPHQL_URL, {
const query = getGraphqlQuery(owner, slug);
const response = await fetch("https://squigglehub.org/api/graphql", {
headers: {
"Content-Type": "application/json",
},
Expand All @@ -42,6 +43,29 @@ export async function fetchCodeFromGraphQL(
return code;
}

// copy-pasted from squiggle/packages/ai/src/scripts/squiggleHubHelpers.ts
export async function fetchCodeFromHub(
owner: string,
slug: string
): Promise<string> {
try {
const data = await fetch(
`${SERVER}/api/get-source?${new URLSearchParams({
owner,
slug,
})}`
).then((res) => res.json());
const parsed = z.object({ code: z.string() }).safeParse(data);
if (!parsed.success) {
throw new Error(`Failed to fetch source for ${owner}/${slug}`);
}

return parsed.data.code;
} catch (e) {
return await fetchCodeFromHubLegacy(owner, slug);
}
}

export const squiggleAiLibraries = defineCollection({
name: "squiggleAiLibraries",
directory: "content/squiggleAiLibraries",
Expand All @@ -52,7 +76,7 @@ export const squiggleAiLibraries = defineCollection({
slug: z.string(),
}),
transform: async (data) => {
const code = await fetchCodeFromGraphQL(data.owner, data.slug);
const code = await fetchCodeFromHub(data.owner, data.slug);
const importName = `hub:${data.owner}/${data.slug}`;

return { ...data, importName, code };
Expand Down
4 changes: 0 additions & 4 deletions packages/hub/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,3 @@
/tsconfig.tsbuildinfo
/.next
/.vscode

# Ignore all of the generated files, but keep the directory
src/__generated__/*
!src/__generated__/.gitkeep
3 changes: 0 additions & 3 deletions packages/hub/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
.vscode
.next
src/__generated__
schema.graphql
test/gql-gen
31 changes: 1 addition & 30 deletions packages/hub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,7 @@ The basic loop is:

## Notes on changing the schema

`pnpm run gen` is a pipeline with three steps:

1. First, `gen:prisma` generates `@prisma/client` from `prisma/schema.prisma`.
2. Then `gen:schema` generates `schema.graphql` from our GraphQL server code in `src/graphql/`.
3. Finally, `gen:relay` runs [relay-compiler](https://relay.dev/docs/guides/compiler/), which generates `src/__generated__` files based on `schema.graphql`.

So:

- for the database schema, `prisma/schema.prisma` is the source of truth
- for the GraphQL schema, the TypeScript code in `src/graphql/` is the source of truth, while `schema.graphql` is auto-generated (but it still should be committed to the repo)
`pnpm run gen` generates `@prisma/client` from `prisma/schema.prisma`.

If it looks like VS Code doesn't see your latest changes, try this:

Expand All @@ -56,18 +47,8 @@ If it looks like VS Code doesn't see your latest changes, try this:

Note: the "Restart TS Server" step is necessary because `@prisma/client` code is out of the main source tree, and VS Code won't notice that it has updated. But restarting TS Server is slow, so a better solution is to keep `@prisma/client` source code open (open `src/prisma.ts`, then "Go to definition" on `PrismaClient`). Then VS Code will watch it for changes.

For Relay-generated files under `src/__generated__`, VS Code usually detects the changes automatically.

Another note is that with the correct setup, out of `pnpm gen:prisma`, `pnpm gen:schema` and `pnpm gen:relay` pipeline steps, only `gen:schema` is necessary:

- `@prisma/client` will be regenerated on `prisma db push` or `prisma migrate dev`
- `gen:schema`, which calls the `src/graphql/print-schema.ts` script, doesn't have the watch mode, so it _is_ necessary to call it after you edit any `src/graphql/` code
- `gen:relay` (`relay-compiler`) will run in watch mode if you use [Relay GraphQL extension](https://marketplace.visualstudio.com/items?itemName=meta.relay) and enable `relay.autoStartCompiler` option in VS Code settings

## Other notes

[How to load GraphQL data in Next.js pages](/docs/relay-pages.md)

[Common workflow for updating Prisma schema](https://www.prisma.io/docs/orm/prisma-migrate/workflows/prototyping-your-schema)

# Deployment
Expand All @@ -79,13 +60,3 @@ Squiggle Hub is deployed on [Vercel](https://vercel.com/) automatically when the
The production database is migrated by [this GitHub Action](https://github.com/quantified-uncertainty/squiggle/blob/main/.github/workflows/prisma-migrate-prod.yml).

**Important: it should be invoked _before_ merging any PR that changes the schema.**

## Debugging

If you get an error like:

```
PothosSchemaError [GraphQLError]: Ref ObjectRef<ModelRevisionRun> has not been implemented
```

Make sure that any new files in `src/graphql/types` have been added to `src/schema.ts`, or something that references that.
14 changes: 0 additions & 14 deletions packages/hub/codegen.ts

This file was deleted.

31 changes: 31 additions & 0 deletions packages/hub/docs/nextjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Notes on Next.js

## Conventions

### Component files

- store single use components in `src/app`, next to their `page.tsx` and `layout.tsx`
- if the component is shared between multiple pages, store it in `src/{topic}/components/`, where `{topic}` is something like "models" or "relative-values"
- if the component doesn't have an obvious topic, e.g. if it's a generic UI component, store it in `src/components/`

### Actions

- store actions in `src/{topic}/actions/`, where `{topic}` is something like "models" or "relative-values"
- name actions like this: `doSomethingAction`
- use `next-safe-action` to define all actions
- return _something_ from actions, even if it's just `"ok"`; some wrappers check whether `data` on the action is defined

### Data loading

- all data loading functions that expose data to the frontend should go in `src/{topic}/data/`
- data loading functions should sanitize the data that they select from the database, to avoid security issues

## Loading pages

Avoid generic `loading.tsx` files. Thoughtful loading states are good, but the generic top-level loading state was harmful:

- it doesn't match the final rendered state so it looks more like a flash of unrelated content than a skeleton
- loading state means that the previous page will disappear faster than necessary, and that's bad; in other words, the loading state is only useful when it hints where the content will appear
- in addition, the loading state means that `<Link>` prefetching won't work

Avoid nested `loading.tsx` files. I'm not sure why but they might cause double flash of loading states, similar to this thread: https://www.reddit.com/r/nextjs/comments/17hn1a5/nested_loading_states/
Loading

0 comments on commit 1ddd150

Please sign in to comment.