Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
4e1cc16
bit-bashing my way to a patchwork proof of concept
pvh Jun 18, 2025
72d01f3
bring in a patchwork solid-js example
pvh Jun 18, 2025
146cdf9
working on the build
pvh Jun 18, 2025
4419682
making progress on an embeding
pvh Jun 18, 2025
0353bd8
getting closer
pvh Jun 18, 2025
f578b61
next error
pvh Jun 18, 2025
6cebcfe
continuing progress
pvh Jun 18, 2025
e27a5e5
got basic models working
pvh Jun 18, 2025
37d0fb5
getting closer
pvh Jun 18, 2025
e3d604f
highlight added cells
paulsonnentag Jul 8, 2025
79b84f1
move patchwork bundle into it's own package
paulsonnentag Jul 9, 2025
decfe92
highlight changed values
paulsonnentag Jul 9, 2025
6e4d25e
create single annotation for changed cell
paulsonnentag Jul 9, 2025
babdc27
render annotations
paulsonnentag Jul 9, 2025
a08bf16
fix: turn patches correctly into added annotations
paulsonnentag Jul 10, 2025
e2efb2c
show deleted cells in changes list
paulsonnentag Jul 10, 2025
cd84eef
add cover element to prevent cells from being editable
paulsonnentag Jul 10, 2025
0e3f5a0
allow to add comments
paulsonnentag Jul 10, 2025
c49f558
add draft AI prompt
geoffreylitt Jul 10, 2025
17e1699
Merge pull request #1 from inkandswitch/ai-prompt
geoffreylitt Jul 10, 2025
59b7794
fix branch switching bug
paulsonnentag Jul 15, 2025
e145ad5
wip: add analysis
paulsonnentag Jul 15, 2025
4960394
make analysis datatype unlisted
paulsonnentag Jul 15, 2025
df01794
create separate tools for model
paulsonnentag Jul 16, 2025
ceaa4c0
fork analysis with model doc
paulsonnentag Jul 16, 2025
cc51382
update reference to model doc from analysis doc when forking
paulsonnentag Jul 16, 2025
8099c03
tweak diff styling
geoffreylitt Jul 23, 2025
8ee1a22
Merge pull request #2 from inkandswitch/diffs
paulsonnentag Jul 23, 2025
d3d9942
implement annotations plugin for analysis and model
paulsonnentag Jul 23, 2025
d736943
read annotations through context
paulsonnentag Jul 23, 2025
0741598
fix resolution of links between model and analysis
paulsonnentag Jul 23, 2025
03bd275
fix reactivity
paulsonnentag Jul 24, 2025
06bed95
fix: use resolved url of analysis
paulsonnentag Jul 24, 2025
68b410f
render diff annotations in sidebar
paulsonnentag Jul 24, 2025
09079be
split annotations by datatype
paulsonnentag Jul 28, 2025
0a77eae
add annotations view for analysis
paulsonnentag Jul 28, 2025
f307325
resolve linked analysisDocUrl correctly on branch
paulsonnentag Jul 28, 2025
8608c32
fix: render diff annotations for analysis
paulsonnentag Jul 28, 2025
6a5600b
implement selection sync
paulsonnentag Jul 28, 2025
7858a1b
tweak annotations style
paulsonnentag Jul 29, 2025
0f07d91
only export side by side
paulsonnentag Jul 29, 2025
aa5a6d9
add comment support
paulsonnentag Jul 30, 2025
7a3bab1
fix: avoid remounting tool
paulsonnentag Aug 4, 2025
b720a4c
fix css import
paulsonnentag Aug 4, 2025
793e523
render cells in review sidebar if they have just a comment without ch…
paulsonnentag Aug 4, 2025
106858f
reenable reconcile
paulsonnentag Aug 4, 2025
ab9532f
fix diff for text cells
paulsonnentag Aug 5, 2025
e5fd367
fix patchesToAnnotations
paulsonnentag Aug 5, 2025
21aa71c
tweak colors
geoffreylitt Aug 8, 2025
c82652d
Enable AI to add cells anywhere in document
geoffreylitt Jul 23, 2025
d17dad4
fix prompt
geoffreylitt Jul 23, 2025
224bce7
vertical scroll
geoffreylitt Aug 8, 2025
ae12625
fix formatting
paulsonnentag Aug 20, 2025
58a10bc
more formatting fixes
paulsonnentag Aug 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/catlog-wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]

[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[dependencies]
all-the-same = "1.1.0"
catlog = { path = "../catlog", features = ["ode", "serde-wasm"] }
Expand Down
9 changes: 5 additions & 4 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.3.0",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
"@automerge/automerge": "^2.2.8",
"@automerge/automerge-repo": "^1.2.1",
"@automerge/automerge-repo-network-websocket": "^1.2.1",
"@automerge/automerge-repo-storage-indexeddb": "^1.2.1",
"@automerge/automerge-repo": "^2.0.7",
"@automerge/automerge-repo-network-websocket": "^2.0.7",
"@automerge/automerge-repo-storage-indexeddb": "^2.0.7",
"@automerge/prosemirror": "^0.0.13",
"@corvu/dialog": "^0.2.4",
"@corvu/disclosure": "^0.2.1",
Expand Down Expand Up @@ -57,7 +57,8 @@
"solid-js": "^1.9.2",
"tiny-invariant": "^1.3.3",
"ts-pattern": "^5.2.0",
"uuid": "^11.0.3"
"uuid": "^11.0.3",
"@patchwork/sdk": "file:../../../patchwork/sdk"
},
"devDependencies": {
"@biomejs/biome": "^1.9.3",
Expand Down
2,632 changes: 2,163 additions & 469 deletions packages/frontend/pnpm-lock.yaml

Large diffs are not rendered by default.

89 changes: 12 additions & 77 deletions packages/frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,32 @@
import { Repo } from "@automerge/automerge-repo";
import { isValidAutomergeUrl, Repo } from "@automerge/automerge-repo";
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { type FirebaseOptions, initializeApp } from "firebase/app";
import invariant from "tiny-invariant";
import * as uuid from "uuid";

import { MultiProvider } from "@solid-primitives/context";
import { Navigate, type RouteDefinition, type RouteSectionProps, Router } from "@solidjs/router";
import { FirebaseProvider } from "solid-firebase";
import { ErrorBoundary, Show, createResource, createSignal, lazy } from "solid-js";

import Dialog, { Content, Portal } from "@corvu/dialog";
import { getAuth, signOut } from "firebase/auth";
import { type Api, ApiContext, createRpcClient, useApi } from "./api";
import { ErrorBoundary, Show, createResource, lazy } from "solid-js";

import { helpRoutes } from "./help/routes";
import { createModel } from "./model/document";
import { PageContainer } from "./page/page_container";
import { TheoryLibraryContext, stdTheories } from "./stdlib";
import { ErrorBoundaryDialog } from "./util/errors";
import { Api, ApiContext, useApi } from "./api";

const serverUrl = import.meta.env.VITE_SERVER_URL;
const repoUrl = import.meta.env.VITE_AUTOMERGE_REPO_URL;
const firebaseOptions = JSON.parse(import.meta.env.VITE_FIREBASE_OPTIONS) as FirebaseOptions;

const Root = (props: RouteSectionProps<unknown>) => {
invariant(serverUrl, "Must set environment variable VITE_SERVER_URL");
invariant(repoUrl, "Must set environment variable VITE_AUTOMERGE_REPO_URL");
const serverHost = new URL(serverUrl).host;

const firebaseApp = initializeApp(firebaseOptions);
const rpc = createRpcClient(serverUrl, firebaseApp);

const repo = new Repo({
storage: new IndexedDBStorageAdapter("catcolab"),
network: [new BrowserWebSocketClientAdapter(repoUrl)],
});

const api: Api = { serverHost, rpc, repo };

const [isSessionInvalid] = createResource(
async () => {
const result = await rpc.validate_session.query();
if (result.tag === "Err") {
await signOut(getAuth(firebaseApp));

// Why this needs to be a separate modal:
// We cannot automatically reload the page because a bug in validate_session might
// trigger an infinite reload loop, so the reload must be user-triggered. Although
// ErrorBoundary might seem like the natural place to handle this, it only catches the
// first error, and there's no guarantee that an error from validate_session will be the
// first one encountered.
return true;
}

return false;
},
{
initialValue: false,
},
);
const api: Api = { repo };

return (
<MultiProvider
Expand All @@ -67,41 +35,13 @@ const Root = (props: RouteSectionProps<unknown>) => {
[TheoryLibraryContext, stdTheories],
]}
>
<FirebaseProvider app={firebaseApp}>
<ErrorBoundary fallback={(err) => <ErrorBoundaryDialog error={err} />}>
<PageContainer>{props.children}</PageContainer>
</ErrorBoundary>
<Show when={isSessionInvalid()}>
<SessionExpiredModal />
</Show>
</FirebaseProvider>
<ErrorBoundary fallback={(err) => <ErrorBoundaryDialog error={err} />}>
<PageContainer>{props.children}</PageContainer>
</ErrorBoundary>
</MultiProvider>
);
};

export function SessionExpiredModal() {
const [reloading, setReloading] = createSignal(false);

const handleReload = () => {
setReloading(true);
location.reload();
};

return (
<Dialog initialOpen={true}>
<Portal>
<Content class="popup error-dialog">
<h3>Session Expired</h3>
<p>Your session is no longer valid. Please reload the page to continue.</p>
<button onClick={handleReload} disabled={reloading()}>
{reloading() ? "Reloading..." : "Reload Page"}
</button>
</Content>
</Portal>
</Dialog>
);
}

function CreateModel() {
const api = useApi();

Expand All @@ -112,7 +52,10 @@ function CreateModel() {
}

const refIsUUIDFilter = {
ref: (ref: string) => uuid.validate(ref),
ref: (ref: string) => {
console.log("ref", ref, isValidAutomergeUrl(ref));
return isValidAutomergeUrl(ref);
},
};

const routes: RouteDefinition[] = [
Expand Down Expand Up @@ -140,14 +83,6 @@ const routes: RouteDefinition[] = [
component: lazy(() => import("./help/help_container")),
children: helpRoutes,
},
{
path: "/dev/*",
component: (props) => {
const url = `https://next.catcolab.org${props.location.pathname}`;
window.location.replace(url);
return null;
},
},
{
path: "/profile",
component: lazy(() => import("./user/profile")),
Expand Down
10 changes: 3 additions & 7 deletions packages/frontend/src/analysis/analysis_editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ export function AnalysisDocumentEditor(props: {
);
}

const AnalysisMenu = (props: {
liveAnalysis: LiveAnalysisDocument;
}) => {
const AnalysisMenu = (props: { liveAnalysis: LiveAnalysisDocument }) => {
const liveDocument = () => {
switch (props.liveAnalysis.analysisType) {
case "diagram":
Expand All @@ -166,9 +164,7 @@ const AnalysisMenu = (props: {
return <DocumentMenu liveDocument={liveDocument()} />;
};

const AnalysisOfPane = (props: {
liveAnalysis: LiveAnalysisDocument;
}) => (
const AnalysisOfPane = (props: { liveAnalysis: LiveAnalysisDocument }) => (
<Switch>
<Match when={props.liveAnalysis.analysisType === "model" && props.liveAnalysis.liveModel}>
{(liveModel) => <ModelPane liveModel={liveModel()} />}
Expand Down Expand Up @@ -213,7 +209,7 @@ export function AnalysisNotebookEditor(props: {
);
}

function AnalysisCellEditor(props: FormalCellEditorProps<Analysis<unknown>>) {
export function AnalysisCellEditor(props: FormalCellEditorProps<Analysis<unknown>>) {
const liveAnalysis = useContext(LiveAnalysisContext);
invariant(liveAnalysis, "Live analysis should be provided as context for cell editor");

Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/analysis/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export type LiveAnalysisDocument = LiveModelAnalysisDocument | LiveDiagramAnalys
export async function createAnalysis(api: Api, analysisType: AnalysisType, analysisOf: StableRef) {
const init = newAnalysisDocument(analysisType, analysisOf);

console.log("init", init);
const result = await api.rpc.new_ref.mutate(init as InterfaceToType<AnalysisDocument>);
invariant(result.tag === "Ok", "Failed to create a new analysis");

Expand Down
39 changes: 14 additions & 25 deletions packages/frontend/src/api/document.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
AutomergeUrl,
type ChangeFn,
type DocHandle,
type DocHandleChangePayload,
type DocumentId,
isValidAutomergeUrl,
Repo,
} from "@automerge/automerge-repo";
import { type Accessor, createEffect, createSignal } from "solid-js";
Expand All @@ -12,7 +14,6 @@ import * as uuid from "uuid";

import type { Permissions } from "catcolab-api";
import type { Document } from "catlog-wasm";
import { PermissionsError } from "../util/errors";
import type { Api } from "./types";

/** An Automerge repo with no networking, used for read-only documents. */
Expand Down Expand Up @@ -54,30 +55,13 @@ and upgrade it to the latest version.
*/
export async function getLiveDoc<Doc extends Document>(
api: Api,
refId: string,
refId: AutomergeUrl,
docType?: string,
): Promise<LiveDoc<Doc>> {
invariant(uuid.validate(refId), () => `Invalid document ref ${refId}`);
const { rpc, repo } = api;

const result = await rpc.get_doc.query(refId);
if (result.tag !== "Ok") {
if (result.code === 403) {
throw new PermissionsError(result.message);
} else {
throw new Error(`Failed to retrieve document: ${result.message}`);
}
}
const refDoc = result.content;

let docHandle: DocHandle<Doc>;
if (refDoc.tag === "Live") {
const docId = refDoc.docId as DocumentId;
docHandle = repo.find(docId) as DocHandle<Doc>;
} else {
const init = refDoc.content as unknown as Doc;
docHandle = localRepo.create(init);
}
invariant(isValidAutomergeUrl(refId), () => `Invalid document ref ${refId}`);
const { repo } = api;

let docHandle = await repo.find<Doc>(refId);

const doc = await makeDocHandleReactive(docHandle);
if (docType !== undefined) {
Expand All @@ -89,21 +73,26 @@ export async function getLiveDoc<Doc extends Document>(

const changeDoc = (f: ChangeFn<Doc>) => docHandle.change(f);

const permissions = refDoc.permissions;
const permissions: Permissions = { anyone: "Own", user: "Own", users: [] };
return { doc, changeDoc, docHandle, permissions };
}

/** Create a Solid Store that tracks an Automerge document.
*/
export async function makeDocHandleReactive<T extends object>(handle: DocHandle<T>): Promise<T> {
const init = await handle.doc();
const init = handle.doc();

const [store, setStore] = createStore<T>(init as T);

const onChange = (payload: DocHandleChangePayload<T>) => {
// Use [`reconcile`](https://www.solidjs.com/tutorial/stores_immutable)
// function to diff the data and thus avoid re-rendering the whole DOM.

setStore(reconcile(payload.doc));

// todo: using reconcile leads to corrupt state when switching branches
// rerendering every time seems fine
// setStore(payload.doc);
};

handle.on("change", onChange);
Expand Down
8 changes: 0 additions & 8 deletions packages/frontend/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import type { Repo } from "@automerge/automerge-repo";

import type { Uuid } from "catlog-wasm";
import type { RpcClient } from "./rpc";

/** Bundle of everything needed to interact with the CatColab backend. */
export type Api = {
/** Host part of the URL for the CatColab backend server. */
serverHost: string;

/** RPC client for the CatColab backend API. */
rpc: RpcClient;

/** Automerge repo connected to the Automerge document server. */
repo: Repo;
};
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/diagram/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Uuid,
} from "catlog-wasm";
import { elaborateDiagram } from "catlog-wasm";

import { type Api, type LiveDoc, type StableRef, getLiveDoc } from "../api";
import { type LiveModelDocument, getLiveModel } from "../model";
import { newNotebook } from "../notebook";
Expand Down
10 changes: 5 additions & 5 deletions packages/frontend/src/help/routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { RouteDefinition } from "@solidjs/router";
import { lazy } from "solid-js";

import { stdTheories } from "../stdlib";
// import { stdTheories } from "../stdlib";
import { lazyMdx } from "../util/mdx";

const theoryWithIdFilter = {
/* const theoryWithIdFilter = {
id: (id: string) => stdTheories.has(id),
};
};*/

export const helpRoutes: RouteDefinition[] = [
{
Expand All @@ -21,11 +21,11 @@ export const helpRoutes: RouteDefinition[] = [
path: "/theories",
component: lazy(() => import("./theories")),
},
{
/*{
path: "/theory/:id",
matchFilters: theoryWithIdFilter,
component: lazy(() => import("./theory")),
},
},*/
{
path: "/quick-intro",
component: lazyMdx(() => import("./quick_intro.mdx")),
Expand Down
12 changes: 9 additions & 3 deletions packages/frontend/src/model/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Uuid,
elaborateModel,
} from "catlog-wasm";

import { type Api, type LiveDoc, getLiveDoc } from "../api";
import { newNotebook } from "../notebook";
import type { TheoryLibrary } from "../stdlib";
Expand Down Expand Up @@ -139,10 +140,15 @@ export async function createModel(
init = initOrTheoryId;
}

const result = await api.rpc.new_ref.mutate(init as InterfaceToType<ModelDocument>);
invariant(result.tag === "Ok", "Failed to create model");
console.log("init", init);
const handle = api.repo.create(init);

/* const result = await api.rpc.new_ref.mutate(
init as InterfaceToType<ModelDocument>
);*/
// invariant(result.tag === "Ok", "Failed to create model");

return result.content;
return handle.documentId;
}

/** Retrieve a model from the backend and make it "live" for editing. */
Expand Down
Loading