Skip to content

Commit

Permalink
📽only one output renderer with nice ipywidgets fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
stevejpurves committed Mar 30, 2023
1 parent 67e9344 commit a487a0a
Show file tree
Hide file tree
Showing 10 changed files with 92 additions and 67 deletions.
35 changes: 18 additions & 17 deletions packages/jupyter/src/jupyter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,39 @@ import { useFetchAnyTruncatedContent } from './hooks';
import type { MinifiedOutput } from 'nbtx';
import { convertToIOutputs } from 'nbtx';
import { fetchAndEncodeOutputImages } from './convertImages';
import type { ThebeCore } from 'thebe-react';
import { useThebeCore } from 'thebe-react';
import { useCellRefRegistry, useNotebookCellExecution, useCellRef } from '@myst-theme/providers';

function OutputRenderer({ id, data, core }: { id: string; data: IOutput[]; core: ThebeCore }) {
const [cell] = useState(new core.PassiveCellRenderer(id));
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
cell.attachToDOM(ref.current);
}, [ref]);
function ActiveOutputRenderer({ cellId, data }: { cellId: string; data: IOutput[] }) {
const { el } = useCellRef(cellId);
const { ready, executing, cell, execute, clear } = useNotebookCellExecution(cellId);

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

return <div data-passive-renderer ref={ref} />;
return null;
}

const MemoOutputRenderer = React.memo(OutputRenderer);

export const NativeJupyterOutputs = ({
export const JupyterOutputs = ({
id,
parent,
outputs,
}: {
id: string;
parent: 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 { register } = useCellRefRegistry();

useEffect(() => {
if (core) return;
Expand All @@ -56,9 +57,9 @@ export const NativeJupyterOutputs = ({
}

return (
<div>
<div ref={register(parent)} data-thebe-ref="true">
{!fullOutputs && <div className="p-2.5">Loading...</div>}
{fullOutputs && core && <MemoOutputRenderer id={id} data={fullOutputs} core={core} />}
{fullOutputs && core && <ActiveOutputRenderer cellId={parent} data={fullOutputs} />}
</div>
);
};
20 changes: 16 additions & 4 deletions packages/jupyter/src/output.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { GenericNode } from 'myst-common';
import type { GenericNode, GenericParent } from 'myst-common';
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 { JupyterOutputs } from './jupyter';
import { KINDS } from './types';

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

Expand Down Expand Up @@ -32,27 +33,38 @@ export function allOutputsAreSafe(
}, true);
}

export function Output(node: GenericNode) {
export function Output(node: GenericNode & { parent: string; context: KINDS }) {
const outputs: MinifiedOutput[] = node.data;
const allSafe = allOutputsAreSafe(outputs, DIRECT_OUTPUT_TYPES, DIRECT_MIME_TYPES);

let component;
if (false) {
component = <SafeOutputs keyStub={node.key} outputs={outputs} />;
} else {
component = <JupyterOutputs id={node.key} outputs={outputs} />;
component = <JupyterOutputs id={node.key} parent={node.parent} outputs={outputs} />;
}

return (
<figure
key={node.key}
id={node.identifier || undefined}
data-cell-id={node.parent}
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',
'text-right': node.align === 'right',
})}
>
<div className="rounded bg-green-500 text-xs text-white px-2 py-1">
[OUTPUT] block id: {node.key} | node.id: {node.id} | node.key: {node.key ?? 'none'}
</div>
{node.context !== KINDS.Notebook && (
<div className="bg-blue-500 text-white">
[Make Interactive] | [Execute Figure] | [Reset]
</div>
)}
{component}
</figure>
);
Expand Down
4 changes: 4 additions & 0 deletions packages/jupyter/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum KINDS {
Article = 'Article',
Notebook = 'Notebook',
}
1 change: 1 addition & 0 deletions packages/providers/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './site';
export * from './tabs';
export * from './xref';
export * from './types';
export * from './notebook';
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import type {
} from 'thebe-core';
import type { IThebeNotebookError, NotebookExecuteOptions } from 'thebe-react';
import { useNotebookBase, useThebeConfig, useThebeCore } from 'thebe-react';
import type { PageLoader } from '../types';
import { KINDS } from '../types';

function notebookFromMdast(core: ThebeCore, config: Config, mdast: GenericParent): ThebeNotebook {
import type { PartialPage } from './types';
import { KINDS } from './types';

export function notebookFromMdast(
core: ThebeCore,
config: Config,
mdast: GenericParent,
): ThebeNotebook {
const rendermime = undefined; // share rendermime beyond notebook scope?
const notebook = new core.ThebeNotebook(mdast.key, config, rendermime);

// no metadata included in mdast yet
//Object.assign(notebook.metadata, ipynb.metadata);

notebook.cells = (mdast.children as GenericParent[]).map((block: GenericParent) => {
if (block.type !== 'block') console.warn(`Unexpected block type ${block.type}`);
if (block.children.length > 0 && block.children[0].type === 'code') {
Expand Down Expand Up @@ -65,6 +68,7 @@ interface NotebookContextType {
options?: NotebookExecuteOptions | undefined,
) => Promise<(IThebeCellExecuteReturn | null)[]>;
notebook: ThebeNotebook | undefined;
registry: CellRefRegistry;
register: (id: string) => (el: HTMLDivElement) => void;
restart: () => Promise<void>;
clear: () => void;
Expand All @@ -76,7 +80,7 @@ export function NotebookProvider({
siteConfig,
page,
children,
}: React.PropsWithChildren<{ siteConfig: boolean; page: PageLoader }>) {
}: React.PropsWithChildren<{ siteConfig: boolean; page: PartialPage }>) {
// so at some point this gets the whole site config and can
// be use to lookup notebooks and recover ThebeNotebooks that
// can be used to execute notebook pages or blocks in articles
Expand All @@ -97,7 +101,7 @@ export function NotebookProvider({
setNotebook,
} = useNotebookBase();

const cellRefs = useRef<CellRefRegistry>({});
const registry = useRef<CellRefRegistry>({});

useEffect(() => {
if (!core || !config || notebook) return;
Expand All @@ -123,12 +127,12 @@ export function NotebookProvider({

function register(id: string) {
return (el: HTMLDivElement) => {
if (el != null && cellRefs.current[id] !== el) {
if (el != null && registry.current[id] !== el) {
if (!el.isConnected) {
console.debug(`skipping ref for cell ${id} as host is not connected`);
} else {
console.debug(`new ref for cell ${id} registered`);
cellRefs.current[id] = el;
registry.current[id] = el;
}
}
};
Expand All @@ -145,6 +149,7 @@ export function NotebookProvider({
executeAll,
executeSome,
notebook,
registry: registry.current,
register,
restart: () => session?.restart() ?? Promise.resolve(),
clear,
Expand All @@ -163,6 +168,16 @@ export function useCellRefRegistry() {
return { register: notebookState.register };
}

export function useCellRef(id: string) {
const notebookState = useContext(NotebookContext);
if (notebookState === undefined) {
throw new Error('useCellRef called outside of NotebookProvider');
}
console.log('useCellRef', { id, registry: notebookState.registry });
const entry = Object.entries(notebookState.registry).find(([cellId]) => cellId === id);
return { el: entry?.[1] ?? null };
}

export function useMDASTNotebook() {
const notebookState = useContext(NotebookContext);

Expand Down
15 changes: 15 additions & 0 deletions packages/providers/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import type React from 'react';
import type { Root } from 'mdast';

// TODO is there a case for a shared types package?
export enum KINDS {
Article = 'Article',
Notebook = 'Notebook',
}

export type PartialPage = {
kind: KINDS;
file: string;
sha256: string;
slug: string;
mdast: Root;
};

export type NodeRenderer<T = any> = (
node: T & { type: string; key: string; html_id?: string },
Expand Down
2 changes: 1 addition & 1 deletion packages/site/src/components/CellRun.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Spinner } from './Spinner';
import { useNotebookCellExecution } from '../pages/NotebookProvider';
import { useNotebookCellExecution } from '@myst-theme/providers';

export function CellRun({ id }: { id: string }) {
const { ready, executing, execute, clear } = useNotebookCellExecution(id);
Expand Down
44 changes: 11 additions & 33 deletions packages/site/src/components/ContentBlocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import type { GenericNode, GenericParent } from 'myst-common';
import type { NodeRenderer } from '@myst-theme/providers';
import { useNodeRenderers } from '@myst-theme/providers';
import classNames from 'classnames';
import { useCellRefRegistry } from '../pages/NotebookProvider';
import { CellRun } from './CellRun';
import { KINDS } from '../types';

function activeCodeRendererFactory(parentId: string, BaseRenderer: NodeRenderer<any>) {
return function ActiveCode(node: GenericNode) {
Expand All @@ -17,42 +17,13 @@ function activeCodeRendererFactory(parentId: string, BaseRenderer: NodeRenderer<
data-mdast-node-type={node.type}
data-mdast-node-id={node.key}
>
{/* <div className="rounded bg-blue-500 text-xs text-white px-2 py-1">
[CODE] block id: {parentId} | node.id: {node.id ?? 'none'} | node.key:{' '}
{node.key ?? 'none'}
</div> */}
{code}
{node.kind !== 'inline' && <CellRun id={parentId} />}
</div>
);
};
}

function activeOutputRendererFactory(parentId: string, BaseRenderer: NodeRenderer<any>) {
// TODO could take a prop here to show some UI to start compute etc...
// then the output is rendered in isolation, without it's code cell
return function ActiveOutput(node: GenericNode) {
const { register } = useCellRefRegistry();
const output = BaseRenderer(node);
return (
<div
className="not-prose"
key={node.key}
data-cell-id={parentId}
data-mdast-node-type={node.type}
data-mdast-node-id={node.key}
>
<div className="rounded bg-green-500 text-xs text-white px-2 py-1">
[OUTPUT] block id: {parentId} | node.id: {node.id} | node.key: {node.key ?? 'none'}
</div>
<div ref={register(parentId)} data-active-output="true">
{output}
</div>
</div>
);
};
}

function ensureCodeBlocksHaveAnOutput(node: GenericParent) {
if (node.children.length === 1 && node.children[0].type === 'code') {
return {
Expand Down Expand Up @@ -80,15 +51,22 @@ function ensureCodeBlocksHaveAnOutput(node: GenericParent) {
};
}

function addParentKey(parent: string, node: GenericParent): GenericParent {
return {
...node,
children: node.children.map((child) => ({ ...child, parent, context: KINDS.Notebook })),
};
}

function Block({ id, node, className }: { id: string; node: GenericParent; className?: string }) {
const { code, output, ...otherRenderers } = useNodeRenderers() ?? DEFAULT_RENDERERS;
const { code, ...otherRenderers } = useNodeRenderers() ?? DEFAULT_RENDERERS;

// TODO - do we need these wrapper components? are we able to push the custom logic
// down into the standard code/output renderers and we decorate the node with data we need?
const children = useParse(ensureCodeBlocksHaveAnOutput(node), {
const children = useParse(addParentKey(id, ensureCodeBlocksHaveAnOutput(node)), {
...otherRenderers,
code: activeCodeRendererFactory(id, code),
output: activeOutputRendererFactory(id, output),
// output: activeOutputRendererFactory(id, output),
});
const subGrid = 'article-grid article-subgrid-gap col-screen';
const dataClassName = typeof node.data?.class === 'string' ? node.data?.class : undefined;
Expand Down
2 changes: 1 addition & 1 deletion packages/site/src/components/NotebookRunAll.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Spinner } from './Spinner';
import { useMDASTNotebook } from '../pages/NotebookProvider';
import { useMDASTNotebook } from '@myst-theme/providers';

export function NotebookRunAll() {
const { ready, attached, notebook, executing, executeAll, restart, clear } = useMDASTNotebook();
Expand Down
3 changes: 1 addition & 2 deletions packages/site/src/pages/Article.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ReferencesProvider } from '@myst-theme/providers';
import { ReferencesProvider, NotebookProvider } from '@myst-theme/providers';
import { FrontmatterBlock } from '@myst-theme/frontmatter';
import { Bibliography, ContentBlocks, FooterLinksBlock } from '../components';
import { ErrorDocumentNotFound } from './ErrorDocumentNotFound';
Expand All @@ -9,7 +9,6 @@ import { ThebeSessionProvider } from 'thebe-react';
import type { GenericParent } from 'myst-common';
import { EnableCompute } from './EnableCompute';
import { NotebookRunAll } from '../components/NotebookRunAll';
import { NotebookProvider } from './NotebookProvider';

export function ArticlePage({ article }: { article: PageLoader }) {
const { hide_title_block, hide_footer_links } = (article.frontmatter as any)?.design ?? {};
Expand Down

0 comments on commit a487a0a

Please sign in to comment.