Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@radix-ui/react-tooltip": "^1.0.7",
"@tailwindcss/container-queries": "^0.1.1",
"@tldraw/tldraw": "2.0.0-alpha.17",
"@wojtekmaj/react-hooks": "^1.19.0",
"@xstate/react": "^3.2.2",
"automerge-tldraw": "0.1.5",
"class-variance-authority": "^0.7.0",
Expand All @@ -53,11 +54,14 @@
"lorem-ipsum": "^2.0.8",
"lucide-react": "^0.284.0",
"openai": "^4.11.0",
"pdfjs-dist": "^4.1.392",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-arborist": "^3.3.1",
"react-dom": "^18.2.0",
"react-pdf": "^7.7.1",
"react-usestateref": "^1.0.8",
"remeda": "^1.60.1",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"use-resize-observer": "^9.1.0",
Expand Down
18 changes: 16 additions & 2 deletions src/DocExplorer/components/DocExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { TLDraw } from "@/tldraw/components/TLDraw";

import queryString from "query-string";
import { setUrlHashForDoc } from "../utils";
import { LivingPapersEditor } from "@/living-papers/components/LivingPapersEditor";

export type Tool = {
id: string;
Expand All @@ -40,6 +41,13 @@ const TOOLS = {
component: TLDraw,
},
],
livingPapers: [
{
id: "livingPapers",
name: "Living Papers",
component: LivingPapersEditor,
},
],
};

export const DocExplorer: React.FC = () => {
Expand All @@ -65,9 +73,9 @@ export const DocExplorer: React.FC = () => {
() => (selectedDocLink ? TOOLS[selectedDocLink.type] : []),
[selectedDocLink]
);
const [activeTool, setActiveTool] = useState(availableTools[0] ?? null);
const [activeTool, setActiveTool] = useState(availableTools?.[0] ?? null);
useEffect(() => {
setActiveTool(availableTools[0]);
setActiveTool(availableTools?.[0]);
}, [availableTools]);

const ToolComponent = activeTool?.component;
Expand Down Expand Up @@ -230,6 +238,12 @@ export const DocExplorer: React.FC = () => {
{selectedDocUrl && selectedDoc && ToolComponent && (
<ToolComponent docUrl={selectedDocUrl} key={selectedDocUrl} />
)}

{!ToolComponent && (
<div className="flex items-center justify-center h-full bg-gray-100 text-gray-500 text-sm cursor-default">
No editor available for this datatype
</div>
)}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/DocExplorer/doctypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TLDrawDatatype } from "@/tldraw/datatype";
import { EssayDatatype } from "@/tee/datatype";
import { LivingPapersDatatype } from "@/living-papers/datatype";

export interface DataType {
id: string;
Expand All @@ -13,6 +14,7 @@ export interface DataType {
export const docTypes = {
essay: EssayDatatype,
tldraw: TLDrawDatatype,
livingPapers: LivingPapersDatatype,
} as const;

export type DocType = keyof typeof docTypes;
79 changes: 79 additions & 0 deletions src/living-papers/components/LivingPapersEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { TinyEssayEditor } from "@/tee/components/TinyEssayEditor";
import { AutomergeUrl } from "@automerge/automerge-repo";
import {
useDocument,
useHandle,
useRepo,
} from "@automerge/automerge-repo-react-hooks";
import { LivingPapersDoc } from "../datatype";
import { PDFViewer } from "./PDFViewer";
import { useCallback, useEffect } from "react";
import { debounce } from "lodash";
import { Build } from "@/tee/lp-shared";

export const LivingPapersEditor = ({ docUrl }: { docUrl: AutomergeUrl }) => {
const [doc, changeDoc] = useDocument<LivingPapersDoc>(docUrl);
const handle = useHandle<LivingPapersDoc>(docUrl);
const repo = useRepo();
const fetchUrl = useCallback(
debounce(async () => {
console.log("rebuild");
// Assuming there's a function to fetch data from a URL
console.log(`Fetching data for docUrl: ${docUrl}`);

const buildResult = await fetch(`http://localhost:8088/build/${docUrl}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔

const amUrl = (await buildResult.text()) as AutomergeUrl;
const buildDoc = (await repo.find(amUrl).doc()) as any; // Assuming BuildsDoc type is known

const build = Object.entries(buildDoc.builds)[0][1] as Build;
const result = build.result;
if (result.ok === false) {
console.error("Build failed", result.error);
return;
}
const buildDirUrl = result.value.buildDirUrl;
const buildDirDoc = (await repo.find(buildDirUrl).doc()) as any;

console.log(buildDirDoc);

const pdf = buildDirDoc["index.pdf"].contents;

console.log("pdf", pdf);

handle.change((d) => (d.pdfOutput = pdf));
}, 1000),
[docUrl, repo]
);

useEffect(() => {
if (doc?.content) {
fetchUrl();
}

// Cleanup function to cancel the debounce if the component unmounts or the doc changes
return () => {
fetchUrl.cancel();
};
}, [fetchUrl, doc.content]);

return (
<div className="flex flex-col h-full">
<div className="bg-gray-100 p-2">Settings go here</div>
<div className="flex-grow flex">
<div className="w-1/2 h-full border-r border-gray-200">
<div className="bg-gray-50 py-2 px-8 text-gray-500 font-bold text-xs">
Source
</div>
<TinyEssayEditor docUrl={docUrl} />
</div>
<div className="w-1/2 h-full bg-gray-50">
<div className="bg-gray-50 py-2 px-8 text-gray-500 font-bold text-xs">
Preview
</div>
{doc.pdfOutput && <PDFViewer data={doc.pdfOutput} />}
{!doc.pdfOutput && <div>No PDF output yet</div>}
</div>
</div>
</div>
);
};
68 changes: 68 additions & 0 deletions src/living-papers/components/PDFViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useMemo, useState } from "react";
import { useResizeObserver } from "@wojtekmaj/react-hooks";
import { pdfjs, Document, Page } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";

// TODO: loading worker from global CDN because Vite import wasn't working,
// fix this.

// pdfjs.GlobalWorkerOptions.workerSrc = new URL(
// "pdfjs-dist/build/pdf.worker.min.js",
// import.meta.url
// ).toString();
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clemens' universal-automerge-cache strategy could come in handy here.


const options = {
cMapUrl: "/cmaps/",
standardFontDataUrl: "/standard_fonts/",
};

const resizeObserverOptions = {};

const maxWidth = 800;

export const PDFViewer = ({ data }: { data: Uint8Array }) => {
const [numPages, setNumPages] = useState<number>();
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>();

const inputToViewer = useMemo(() => ({ data: data.slice(0) }), [data]);

const onResize = useCallback<ResizeObserverCallback>((entries) => {
const [entry] = entries;

if (entry) {
setContainerWidth(entry.contentRect.width);
}
}, []);

useResizeObserver(containerRef, resizeObserverOptions, onResize);

// todo: get TS to understand the expected type for this callback
function onDocumentLoadSuccess(pdfDocumentProxy: any): void {
setNumPages(pdfDocumentProxy.numPages);
}

return (
<div className="w-full max-w-[calc(100%-2em)] my-4" ref={setContainerRef}>
<Document
file={inputToViewer}
onLoadSuccess={onDocumentLoadSuccess}
options={options}
className="flex flex-col items-center"
>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={
containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth
}
className="border border-gray-200"
/>
))}
</Document>
</div>
);
};
44 changes: 44 additions & 0 deletions src/living-papers/datatype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { MarkdownDoc } from "@/tee/schema";
import * as MarkdownDatatype from "@/tee/datatype";

import { BookIcon } from "lucide-react";

// Until Patchwork has better multi-doc versioning, we have to keep the
// TEE Markdown content directly in this doc for basic usability.
export type LivingPapersDoc = MarkdownDoc & {
pdfOutput: Uint8Array;
};

// When a copy of the document has been made,
// update the title so it's more clear which one is the copy vs original.
// (this mechanism needs to be thought out more...)
export const markCopy = (doc: any) => {
MarkdownDatatype.markCopy(doc);
};

const getTitle = (doc: any) => {
return MarkdownDatatype.getTitle(doc);
};

export const init = (doc: any) => {
const initContent = `---
title: Untitled Living Paper
author:
- name: The Living Papers Team
org: University of Washington
keywords: [all, about, my, article]
output:
latex: true
---`;

doc.content = initContent;
};

export const LivingPapersDatatype = {
id: "living-papers",
name: "Living Paper",
icon: BookIcon,
init,
getTitle,
markCopy, // TODO: this shouldn't be here
};
46 changes: 46 additions & 0 deletions src/living-papers/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isValidAutomergeUrl, Repo } from "@automerge/automerge-repo";
import { BroadcastChannelNetworkAdapter } from "@automerge/automerge-repo-network-broadcastchannel";
import { BrowserWebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";

import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb";
import { next as Automerge } from "@automerge/automerge";

import { mount } from "./mount.js";
import "./index.css";
import { LivingPapersDoc } from "./datatype.js";

const SYNC_SERVER_URL =
import.meta.env?.VITE_SYNC_SERVER_URL ?? "wss://sync.automerge.org";

const repo = new Repo({
network: [
new BroadcastChannelNetworkAdapter(),
new BrowserWebSocketClientAdapter(SYNC_SERVER_URL),
],
storage: new IndexedDBStorageAdapter(),
});

const rootDocUrl = `${document.location.hash.slice(1)}`;
let handle;
if (isValidAutomergeUrl(rootDocUrl)) {
handle = repo.find(rootDocUrl);
} else {
handle = repo.create<LivingPapersDoc>();
const { init } = await import("./datatype.js");
handle.change(init);
}

// eslint-disable-next-line
const docUrl = (document.location.hash = handle.url);

// @ts-expect-error - adding property to window
window.Automerge = Automerge;
// @ts-expect-error - adding property to window
window.repo = repo;
// @ts-expect-error - adding property to window
window.handle = handle; // we'll use this later for experimentation

// @ts-expect-error - adding property to window
window.logoImageUrl = "/assets/logo-favicon-310x310-transparent.png";

mount(document.getElementById("root"), { docUrl });
23 changes: 23 additions & 0 deletions src/living-papers/mount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import ReactDom from "react-dom/client";
import { RepoContext } from "@automerge/automerge-repo-react-hooks";
import { LivingPapersEditor } from "./components/LivingPapersEditor";

export function mount(node, params) {
// workaround different conventions for documentUrl
if (!params.docUrl && params.documentUrl) {
params.docUrl = params.documentUrl;
}

ReactDom.createRoot(node).render(
// We get the Automerge Repo from the global window;
// this is set by either our standalone entrypoint or trailrunner
React.createElement(
RepoContext.Provider,
// eslint-disable-next-line no-undef
// @ts-expect-error - repo is on window
{ value: repo },
React.createElement(LivingPapersEditor, Object.assign({}, params))
)
);
}
7 changes: 1 addition & 6 deletions src/tee/components/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import React, { useEffect, useRef, useState } from "react";

import {
EditorView,
keymap,
drawSelection,
dropCursor,
} from "@codemirror/view";
import { EditorView, keymap, dropCursor } from "@codemirror/view";
import { markdown } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";

Expand Down
6 changes: 4 additions & 2 deletions src/tee/datatype.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,17 @@ export const getTitle = (doc: any) => {
const frontmatterMatch = content.match(frontmatterRegex);
const frontmatter = frontmatterMatch ? frontmatterMatch[1] : "";

const titleRegex = /title:\s"(.+?)"/;
const subtitleRegex = /subtitle:\s"(.+?)"/;
const titleRegex = /title:\s"?(.+?)"?\n/;
const subtitleRegex = /subtitle:\s"?(.+?)"?\n/;

const titleMatch = frontmatter.match(titleRegex);
const subtitleMatch = frontmatter.match(subtitleRegex);

let title = titleMatch ? titleMatch[1] : null;
const subtitle = subtitleMatch ? subtitleMatch[1] : "";

console.log(doc, frontmatter, title, subtitle);

// If title not found in frontmatter, find first markdown heading
if (!title) {
const titleFallbackRegex = /(^|\n)#\s(.+)/;
Expand Down
Loading