Skip to content

Commit

Permalink
🪐 Thebe integration (#21)
Browse files Browse the repository at this point in the history
* 👆🏻moved core provider up to Root, trigger intial load from first output rendered
* session available on each article page
* named sessions mean separate sessions per page
* ✨ initial notebook provider with thebe
* ✨ initial notebook provider with thebe
* break compute, but favours initial widgets fallbacks
* 📽only one output renderer with nice ipywidgets fallback
* 🥌 no component wrapping, base node renderers now doing tha' thing
* ✨ ux improvements
* 🎯 moved `thebe` providers to `jupyter`
* 🔔 passive and active renderers in place
* 🎚disable compute when no thebe options
* 🚀 launch in jupyter
* 📡 launch in binder badge
* 🛠 fix alignment on badge for articles
* 🎑 better passive rendering for widgets
* 👊🏽 bump `thebe-react`
* 🧹 tidy
* 📇 named group hover for notebook cells
* 🪄 remove 2 extra page renders on outward transition
* import heroicons singularly
* 👊🏽 `thebe-react`
* 📏 sized frontmatter header track to avoid jitter
* 🔧 Consume new thebe myst frontmatter (#57)
* 📦 Remove deps from frontmatter

Co-authored-by: stevejpurves <steve@curvenote.com>
Co-authored-by: Franklin Koch <franklinwkoch@gmail.com>
Co-authored-by: Rowan Cockett <rowanc1@gmail.com>
  • Loading branch information
3 people authored Apr 26, 2023
1 parent 0858c07 commit ae4848d
Show file tree
Hide file tree
Showing 31 changed files with 3,626 additions and 2,281 deletions.
9 changes: 9 additions & 0 deletions .changeset/rotten-rules-juggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'myst-to-react': patch
'@myst-theme/frontmatter': patch
'@myst-theme/providers': patch
'@myst-theme/jupyter': patch
'@myst-theme/site': patch
---

Add Thebe to theme
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
[submodule "themes/article-theme"]
path = themes/article-theme
url = https://github.com/executablebooks/myst-article-theme
[submodule "patches"]
path = patches
url = https://github.com/executablebooks/myst-theme-patches
4,920 changes: 2,811 additions & 2,109 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"docs"
],
"scripts": {
"postinstall": "patch-package --patch-dir patches",
"compile": "turbo run compile",
"build": "turbo run build",
"dev": "turbo run dev --parallel --filter='./packages/*'",
Expand Down Expand Up @@ -36,6 +37,7 @@
"@types/react-dom": "^18.0.10",
"eslint-config-curvenote": "latest",
"jest": "^29.5.0",
"patch-package": "^6.5.1",
"prettier": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand All @@ -54,5 +56,6 @@
"react-dom": "^18.2.0"
}
},
"packageManager": "npm@8.10.0"
"packageManager": "npm@8.10.0",
"dependencies": {}
}
6 changes: 3 additions & 3 deletions packages/frontmatter/src/FrontmatterBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export function FrontmatterBlock({
return (
<>
{hasHeaders && (
<div className="flex mt-3 mb-5 text-sm font-light">
<div className="flex mt-3 mb-5 text-sm font-light items-center h-6">
{subject && (
<div
className={classNames('flex-none pr-2 smallcaps', {
Expand All @@ -306,9 +306,9 @@ export function FrontmatterBlock({
<OpenAccessBadge open_access={open_access} />
<GitHubLink github={github} />
{isJupyter && (
<span>
<div className="inline-block mr-1">
<JupyterIcon className="h-5 w-5 inline-block" />
</span>
</div>
)}
<DownloadsDropdown exports={exports as any} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/frontmatter/src/downloads.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function Download({
export function DownloadsDropdown({ exports }: HasExports) {
if (!exports || exports.length === 0) return null;
return (
<Menu as="div" className="relative grow-0 inline-block mx-1">
<Menu as="div" className="flex relative grow-0 inline-block mx-1">
<Menu.Button className="relative">
<span className="sr-only">Downloads</span>
<ArrowDownTrayIcon className="w-5 h-5 ml-2 -mr-1" aria-hidden="true" />
Expand Down
10 changes: 0 additions & 10 deletions packages/jupyter/jest.config.js

This file was deleted.

9 changes: 5 additions & 4 deletions packages/jupyter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
"build:esm": "tsc --module es2020 --outDir dist/esm",
"build:types": "tsc --declaration --emitDeclarationOnly --declarationMap --outDir dist/types",
"build": "npm-run-all -l clean -p build:cjs build:esm build:types",
"test": "jest"
"build": "npm-run-all -l clean -p build:cjs build:esm build:types"
},
"dependencies": {
"@headlessui/react": "^1.7.13",
Expand All @@ -28,14 +27,16 @@
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"myst-common": "^0.0.16",
"myst-config": "^0.0.12",
"myst-config": "^0.0.14",
"myst-frontmatter": "^0.0.13",
"myst-spec": "^0.0.4",
"nanoid": "^4.0.0",
"nbtx": "^0.2.3",
"react-popper": "^2.3.0",
"react-syntax-highlighter": "^15.5.0",
"swr": "^1.3.0",
"thebe-core": "^0.1.1",
"thebe-core": "^0.1.6",
"thebe-react": "^0.0.7",
"unist-util-select": "^4.0.1"
},
"peerDependencies": {
Expand Down
75 changes: 75 additions & 0 deletions packages/jupyter/src/BinderBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
function BinderBadgeLogo() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="109" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stopColor="#bbb" stopOpacity=".1" />
<stop offset="1" stopOpacity=".1" />
</linearGradient>
<clipPath id="a">
<rect width="109" height="20" fill="#fff" rx="3" />
</clipPath>
<g clipPath="url(#a)">
<path fill="#555" d="M0 0h64v20H0z" />
<path fill="#579aca" d="M64 0h45v20H64z" />
<path fill="url(#b)" d="M0 0h109v20H0z" />
</g>
<g
fill="#fff"
fontFamily="DejaVu Sans,Verdana,Geneva,sans-serif"
fontSize="110"
textAnchor="middle"
>
<image
width="14"
height="14"
x="5"
y="3"
href="data:image/png;base64,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"
/>
<text
x="415"
y="150"
fill="#010101"
fillOpacity=".3"
textLength="370"
transform="scale(.1)"
>
launch
</text>
<text x="415" y="140" textLength="370" transform="scale(.1)">
launch
</text>
<text
x="855"
y="150"
fill="#010101"
fillOpacity=".3"
textLength="350"
transform="scale(.1)"
>
binder
</text>
<text x="855" y="140" textLength="350" transform="scale(.1)">
binder
</text>
</g>
</svg>
);
}

export function BinderBadge({ binder }: { binder?: string }) {
if (!binder) return null;
return (
<div className="inline-block mr-1 opacity-80 hover:opacity-100">
<a
href={binder}
title={`Launch Binder Session: ${binder}`}
target="_blank"
rel="noopener noreferrer"
className="text-inherit hover:text-inherit"
>
<BinderBadgeLogo />
</a>
</div>
);
}
2 changes: 1 addition & 1 deletion packages/jupyter/src/ClientOnly.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import type { ReactNode } from 'react';

export default function ClientOnly({ children }: { children: ReactNode }) {
export function ClientOnly({ children }: { children: ReactNode }) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
Expand Down
3 changes: 3 additions & 0 deletions packages/jupyter/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ const OUTPUT_RENDERERS = {
output: Output,
};

export * from './BinderBadge';
export * from './providers';

export default OUTPUT_RENDERERS;
85 changes: 66 additions & 19 deletions packages/jupyter/src/jupyter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,66 @@ import { useFetchAnyTruncatedContent } from './hooks';
import type { MinifiedOutput } from 'nbtx';
import { convertToIOutputs } from 'nbtx';
import { fetchAndEncodeOutputImages } from './convertImages';
import type { ThebeCore } from './thebe-provider';
import { useThebeCore } from './thebe-provider';
import type { ThebeCore } from 'thebe-core';
import { useThebeCore } from 'thebe-react';
import { useCellRef, useCellRefRegistry, useNotebookCellExecution } from './providers';
import { SourceFileKind } from 'myst-common';

function OutputRenderer({ id, data, core }: { id: string; data: IOutput[]; core: ThebeCore }) {
const [cell] = useState(new core.PassiveCellRenderer(id));
function ActiveOutputRenderer({ id, data }: { id: string; data: IOutput[] }) {
const ref = useCellRef(id);
const exec = useNotebookCellExecution(id);

useEffect(() => {
if (!ref?.el || !exec?.cell) return;
console.debug(`Attaching cell ${exec.cell.id} to DOM at:`, {
el: ref.el,
connected: ref.el.isConnected,
data,
});
exec.cell.attachToDOM(ref.el);
exec.cell.render(data);
}, [ref?.el, exec?.cell]);

return null;
}

function PassiveOutputRenderer({
id,
data,
core,
kind,
}: {
id: string;
data: IOutput[];
core: ThebeCore;
kind: SourceFileKind;
}) {
const [cell] = useState(new core.PassiveCellRenderer(id, undefined, undefined));
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
cell.render(data);
cell.render(data, kind === SourceFileKind.Article);
}, [data, cell]);
useEffect(() => {
if (!ref.current) return;
cell.attachToDOM(ref.current);
cell.attachToDOM(ref.current, true);
}, [ref]);
return <div ref={ref} />;
return <div ref={ref} data-thebe-passive-ref="true" />;
}

const MemoOutputRenderer = React.memo(OutputRenderer);
const MemoPassiveOutputRenderer = React.memo(PassiveOutputRenderer);

export const NativeJupyterOutputs = ({
id,
outputs,
}: {
id: string;
outputs: MinifiedOutput[];
}) => {
const { core } = useThebeCore();
export const JupyterOutputs = ({ id, outputs }: { id: string; outputs: MinifiedOutput[] }) => {
const { core, load } = useThebeCore();
const { data, error } = useFetchAnyTruncatedContent(outputs);
const [loaded, setLoaded] = useState(false);
const [fullOutputs, setFullOutputs] = useState<IOutput[] | null>(null);
const registry = useCellRefRegistry();
const exec = useNotebookCellExecution(id);

useEffect(() => {
if (core) return;
load();
}, [core, load]);

useEffect(() => {
if (!data || loaded) return;
Expand All @@ -47,10 +78,26 @@ export const NativeJupyterOutputs = ({
return <div className="text-red-500">Error rendering output: {error.message}</div>;
}

if (registry && exec?.cell) {
return (
<div ref={registry?.register(id)} data-thebe-active-ref="true">
{!fullOutputs && <div className="p-2.5">Loading...</div>}
{fullOutputs && <ActiveOutputRenderer id={id} data={fullOutputs} />}
</div>
);
}

return (
<div>
<>
{!fullOutputs && <div className="p-2.5">Loading...</div>}
{fullOutputs && core && <MemoOutputRenderer id={id} core={core} data={fullOutputs} />}
</div>
{fullOutputs && core && (
<MemoPassiveOutputRenderer
id={id}
data={fullOutputs}
core={core}
kind={exec?.kind ?? SourceFileKind.Notebook}
></MemoPassiveOutputRenderer>
)}
</>
);
};
19 changes: 7 additions & 12 deletions packages/jupyter/src/output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { KnownCellOutputMimeTypes } from 'nbtx';
import type { MinifiedMimeOutput, MinifiedOutput } from 'nbtx';
import classNames from 'classnames';
import { SafeOutputs } from './safe';
import { NativeJupyterOutputs as JupyterOutputs } from './jupyter';
import ClientOnly from './ClientOnly';
import { ThebeCoreProvider } from './thebe-provider';
import { JupyterOutputs } from './jupyter';
import { useNotebookCellExecution } from './providers';

export const DIRECT_OUTPUT_TYPES = new Set(['stream', 'error']);

Expand Down Expand Up @@ -35,27 +34,23 @@ export function allOutputsAreSafe(
}

export function Output(node: GenericNode) {
const exec = useNotebookCellExecution(node.key);
const outputs: MinifiedOutput[] = node.data;
const allSafe = allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES);

let component;
if (allSafe) {
if (allSafe && !exec?.ready) {
component = <SafeOutputs keyStub={node.key} outputs={outputs} />;
} else {
// Hide the iframe if rendering on the server
component = (
<ClientOnly>
<ThebeCoreProvider>
<JupyterOutputs id={node.key} outputs={outputs} />
</ThebeCoreProvider>
</ClientOnly>
);
component = <JupyterOutputs id={node.key} outputs={outputs} />;
}

return (
<figure
key={node.key}
id={node.identifier || undefined}
data-mdast-node-type={node.type}
data-mdast-node-id={node.key}
className={classNames('max-w-full overflow-auto m-0 group not-prose relative', {
'text-left': !node.align || node.align === 'left',
'text-center': node.align === 'center',
Expand Down
Loading

0 comments on commit ae4848d

Please sign in to comment.