Skip to content

Commit 87fac03

Browse files
authored
Extract IframeOutput component (#728)
1 parent 5defa00 commit 87fac03

3 files changed

Lines changed: 83 additions & 80 deletions

File tree

src/components/notebook/cell/MarkdownCell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import React, {
1515
useState,
1616
} from "react";
1717

18-
import { IframeOutput } from "@/components/outputs/MaybeCellOutputs.js";
18+
import { IframeOutput } from "@/components/outputs/IframeOutput.js";
1919
import { Button } from "@/components/ui/button.js";
2020
import { useFeatureFlag } from "@/contexts/FeatureFlagContext.js";
2121
import { useUserRegistry } from "@/hooks/useUserRegistry.js";
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { CellType, OutputData } from "@runtimed/schema";
2+
import { useState, useRef, useEffect } from "react";
3+
import { useDebounce } from "react-use";
4+
import { useIframeCommsParent } from "./shared-with-iframe/comms";
5+
6+
export interface IframeOutputProps {
7+
outputs: OutputData[];
8+
style?: React.CSSProperties;
9+
className?: string;
10+
onHeightChange?: (height: number) => void;
11+
isReact?: boolean;
12+
defaultHeight?: string;
13+
onDoubleClick?: () => void;
14+
onMarkdownRendered?: () => void;
15+
cellType?: CellType;
16+
}
17+
18+
export const IframeOutput: React.FC<IframeOutputProps> = ({
19+
outputs,
20+
className,
21+
style,
22+
isReact,
23+
onHeightChange,
24+
defaultHeight = "0px",
25+
onDoubleClick,
26+
onMarkdownRendered,
27+
cellType,
28+
}) => {
29+
const { iframeRef, iframeHeight } = useIframeCommsParent({
30+
defaultHeight,
31+
onHeightChange,
32+
outputs,
33+
onDoubleClick,
34+
onMarkdownRendered,
35+
});
36+
37+
const [debouncedIframeHeight, setDebouncedIframeHeight] =
38+
useState(iframeHeight);
39+
40+
// Iframe can get height updates pretty often, but we want to avoid layout jumping each time
41+
// TODO: ensure that it's a leading debounce!
42+
useDebounce(() => setDebouncedIframeHeight(iframeHeight), 50, [iframeHeight]);
43+
44+
const isAiCell = cellType === "ai";
45+
const scrollContainerRef = useRef<HTMLDivElement>(null);
46+
47+
// Auto-scroll to bottom when content changes for AI cells
48+
useEffect(() => {
49+
if (isAiCell && scrollContainerRef.current) {
50+
const container = scrollContainerRef.current;
51+
container.scrollTop = container.scrollHeight;
52+
}
53+
}, [isAiCell, outputs, debouncedIframeHeight]);
54+
55+
const iframeElement = (
56+
<iframe
57+
src={
58+
import.meta.env.VITE_IFRAME_OUTPUT_URI + (isReact ? "/react.html" : "")
59+
}
60+
ref={iframeRef}
61+
className={className}
62+
width="100%"
63+
height={debouncedIframeHeight}
64+
style={style}
65+
allow="accelerometer; autoplay; gyroscope; magnetometer; xr-spatial-tracking; clipboard-write; fullscreen"
66+
sandbox="allow-downloads allow-forms allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-modals allow-top-navigation-by-user-activation"
67+
loading="lazy"
68+
/>
69+
);
70+
71+
if (isAiCell) {
72+
return (
73+
<div ref={scrollContainerRef} className="max-h-[30vh] overflow-y-auto">
74+
{iframeElement}
75+
</div>
76+
);
77+
}
78+
79+
return iframeElement;
80+
};

src/components/outputs/MaybeCellOutputs.tsx

Lines changed: 2 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import { outputsDeltasQuery, processDeltas } from "@/queries/outputDeltas";
33
import { CellType, OutputData, SAFE_MIME_TYPES } from "@runtimed/schema";
44
import { groupConsecutiveStreamOutputs } from "@/util/output-grouping";
55
import { useQuery } from "@livestore/react";
6-
import { useMemo, useState, useRef, useEffect } from "react";
7-
import { useIframeCommsParent } from "./shared-with-iframe/comms";
6+
import { useMemo } from "react";
87
import { SingleOutput } from "./shared-with-iframe/SingleOutput";
9-
import { useDebounce } from "react-use";
108
import { OutputsContainer } from "./shared-with-iframe/OutputsContainer";
119
import { SuspenseSpinner } from "./shared-with-iframe/SuspenseSpinner";
1210
import { MaybeFixCodeButton } from "./MaybeFixCodeButton";
11+
import { IframeOutput } from "./IframeOutput";
1312

1413
/**
1514
* TODO: consider renaming this component
@@ -88,82 +87,6 @@ export const MaybeCellOutputs = ({
8887
);
8988
};
9089

91-
interface IframeOutputProps {
92-
outputs: OutputData[];
93-
style?: React.CSSProperties;
94-
className?: string;
95-
onHeightChange?: (height: number) => void;
96-
isReact?: boolean;
97-
defaultHeight?: string;
98-
onDoubleClick?: () => void;
99-
onMarkdownRendered?: () => void;
100-
cellType?: CellType;
101-
}
102-
103-
export const IframeOutput: React.FC<IframeOutputProps> = ({
104-
outputs,
105-
className,
106-
style,
107-
isReact,
108-
onHeightChange,
109-
defaultHeight = "0px",
110-
onDoubleClick,
111-
onMarkdownRendered,
112-
cellType,
113-
}) => {
114-
const { iframeRef, iframeHeight } = useIframeCommsParent({
115-
defaultHeight,
116-
onHeightChange,
117-
outputs,
118-
onDoubleClick,
119-
onMarkdownRendered,
120-
});
121-
122-
const [debouncedIframeHeight, setDebouncedIframeHeight] =
123-
useState(iframeHeight);
124-
125-
// Iframe can get height updates pretty often, but we want to avoid layout jumping each time
126-
// TODO: ensure that it's a leading debounce!
127-
useDebounce(() => setDebouncedIframeHeight(iframeHeight), 50, [iframeHeight]);
128-
129-
const isAiCell = cellType === "ai";
130-
const scrollContainerRef = useRef<HTMLDivElement>(null);
131-
132-
// Auto-scroll to bottom when content changes for AI cells
133-
useEffect(() => {
134-
if (isAiCell && scrollContainerRef.current) {
135-
const container = scrollContainerRef.current;
136-
container.scrollTop = container.scrollHeight;
137-
}
138-
}, [isAiCell, outputs, debouncedIframeHeight]);
139-
140-
const iframeElement = (
141-
<iframe
142-
src={
143-
import.meta.env.VITE_IFRAME_OUTPUT_URI + (isReact ? "/react.html" : "")
144-
}
145-
ref={iframeRef}
146-
className={className}
147-
width="100%"
148-
height={debouncedIframeHeight}
149-
style={style}
150-
allow="accelerometer; autoplay; gyroscope; magnetometer; xr-spatial-tracking; clipboard-write; fullscreen"
151-
sandbox="allow-downloads allow-forms allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-modals allow-top-navigation-by-user-activation"
152-
loading="lazy"
153-
/>
154-
);
155-
156-
if (isAiCell) {
157-
return (
158-
<div ref={scrollContainerRef} className="max-h-[30vh] overflow-y-auto">
159-
{iframeElement}
160-
</div>
161-
);
162-
}
163-
164-
return iframeElement;
165-
};
166-
16790
const hasUnsafeOutputs = (outputs: OutputData[]) => {
16891
return outputs.some((output) => {
16992
return !SAFE_MIME_TYPES.includes(output.mimeType as any);

0 commit comments

Comments
 (0)