fix: prevent empty code execution outputs after remount (#678)
This commit is contained in:
parent
1df0181035
commit
3e34d678b3
2 changed files with 51 additions and 85 deletions
|
|
@ -124,14 +124,16 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||||
consoleOutput.status,
|
consoleOutput.status,
|
||||||
) ? (
|
) ? (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
<div className="animate-spin size-fit self-center">
|
<div className="animate-spin size-fit self-center mb-auto mt-0.5">
|
||||||
<LoaderIcon />
|
<LoaderIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted-foreground">
|
<div className="text-muted-foreground">
|
||||||
{consoleOutput.status === 'in_progress'
|
{consoleOutput.status === 'in_progress'
|
||||||
? 'Initializing...'
|
? 'Initializing...'
|
||||||
: consoleOutput.status === 'loading_packages'
|
: consoleOutput.status === 'loading_packages'
|
||||||
? 'Loading Packages...'
|
? consoleOutput.contents.map((content) =>
|
||||||
|
content.type === 'text' ? content.value : null,
|
||||||
|
)
|
||||||
: null}
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,49 +6,17 @@ import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
memo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { ConsoleOutput, UIBlock } from './block';
|
import type { ConsoleOutput, ConsoleOutputContent, UIBlock } from './block';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { PlayIcon } from './icons';
|
import { PlayIcon } from './icons';
|
||||||
|
import { useBlockSelector } from '@/hooks/use-block';
|
||||||
function detectPythonImports(code: string, pyodide: any): Set<string> {
|
|
||||||
const imports = new Set<string>();
|
|
||||||
|
|
||||||
const importPatterns = [
|
|
||||||
/import\s+(\w+)(?:\s+as\s+\w+)?/g,
|
|
||||||
/from\s+(\w+(?:\.\w+)*)\s+import/g,
|
|
||||||
/import\s+(\w+(?:\.\w+)*)/g,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const pattern of importPatterns) {
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
match = pattern.exec(code);
|
|
||||||
if (match === null) break;
|
|
||||||
|
|
||||||
const rootPackage = match[1].split('.')[0];
|
|
||||||
if (rootPackage) {
|
|
||||||
imports.add(rootPackage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get standard libraries dynamically when initializing Pyodide
|
|
||||||
let standardLibs = new Set<string>();
|
|
||||||
if (pyodide) {
|
|
||||||
const stdLibModules = pyodide.runPython(`
|
|
||||||
import sys
|
|
||||||
list(sys.stdlib_module_names)
|
|
||||||
`);
|
|
||||||
standardLibs = new Set(stdLibModules);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Set(Array.from(imports).filter((pkg) => !standardLibs.has(pkg)));
|
|
||||||
}
|
|
||||||
|
|
||||||
const OUTPUT_HANDLERS = {
|
const OUTPUT_HANDLERS = {
|
||||||
matplotlib: `
|
matplotlib: `
|
||||||
|
import io
|
||||||
|
import base64
|
||||||
from matplotlib import pyplot as plt
|
from matplotlib import pyplot as plt
|
||||||
|
|
||||||
# Clear any existing plots
|
# Clear any existing plots
|
||||||
|
|
@ -91,19 +59,23 @@ function detectRequiredHandlers(code: string): string[] {
|
||||||
return handlers;
|
return handlers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RunCodeButton({
|
export function PureRunCodeButton({
|
||||||
block,
|
|
||||||
setConsoleOutputs,
|
setConsoleOutputs,
|
||||||
}: {
|
}: {
|
||||||
block: UIBlock;
|
block: UIBlock;
|
||||||
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
||||||
}) {
|
}) {
|
||||||
const [pyodide, setPyodide] = useState<any>(null);
|
|
||||||
const isPython = true;
|
const isPython = true;
|
||||||
const codeContent = block.content;
|
const [pyodide, setPyodide] = useState<any>(null);
|
||||||
|
|
||||||
|
const codeContent = useBlockSelector((state) => state.content);
|
||||||
|
const isBlockStreaming = useBlockSelector(
|
||||||
|
(state) => state.status === 'streaming',
|
||||||
|
);
|
||||||
|
|
||||||
const loadAndRunPython = useCallback(async () => {
|
const loadAndRunPython = useCallback(async () => {
|
||||||
const runId = generateUUID();
|
const runId = generateUUID();
|
||||||
|
const stdOutputs: Array<ConsoleOutputContent> = [];
|
||||||
|
|
||||||
setConsoleOutputs((outputs) => [
|
setConsoleOutputs((outputs) => [
|
||||||
...outputs,
|
...outputs,
|
||||||
|
|
@ -120,41 +92,38 @@ export function RunCodeButton({
|
||||||
try {
|
try {
|
||||||
if (!currentPyodideInstance) {
|
if (!currentPyodideInstance) {
|
||||||
// @ts-expect-error - loadPyodide is not defined
|
// @ts-expect-error - loadPyodide is not defined
|
||||||
const newPyodideInstance = await loadPyodide({
|
const newPyodideInstance = await globalThis.loadPyodide({
|
||||||
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
|
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setPyodide(null);
|
||||||
setPyodide(newPyodideInstance);
|
setPyodide(newPyodideInstance);
|
||||||
currentPyodideInstance = newPyodideInstance;
|
currentPyodideInstance = newPyodideInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
await currentPyodideInstance.runPythonAsync(`
|
currentPyodideInstance.setStdout({
|
||||||
import sys, io, gc
|
batched: (output: string) => {
|
||||||
import base64
|
stdOutputs.push({
|
||||||
|
type: output.startsWith('data:image/png;base64')
|
||||||
|
? 'image'
|
||||||
|
: 'text',
|
||||||
|
value: output,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
sys.stdout = io.StringIO()
|
await currentPyodideInstance.loadPackagesFromImports(codeContent, {
|
||||||
`);
|
messageCallback: (message: string) => {
|
||||||
|
|
||||||
// Detect and load required packages
|
|
||||||
const requiredPackages = detectPythonImports(
|
|
||||||
codeContent,
|
|
||||||
currentPyodideInstance,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requiredPackages.size > 0) {
|
|
||||||
setConsoleOutputs((outputs) => [
|
setConsoleOutputs((outputs) => [
|
||||||
...outputs.filter((output) => output.id !== runId),
|
...outputs.filter((output) => output.id !== runId),
|
||||||
{
|
{
|
||||||
id: runId,
|
id: runId,
|
||||||
contents: [],
|
contents: [{ type: 'text', value: message }],
|
||||||
status: 'loading_packages',
|
status: 'loading_packages',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
},
|
||||||
await currentPyodideInstance.loadPackage(
|
});
|
||||||
Array.from(requiredPackages),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requiredHandlers = detectRequiredHandlers(codeContent);
|
const requiredHandlers = detectRequiredHandlers(codeContent);
|
||||||
for (const handler of requiredHandlers) {
|
for (const handler of requiredHandlers) {
|
||||||
|
|
@ -173,24 +142,11 @@ export function RunCodeButton({
|
||||||
|
|
||||||
await currentPyodideInstance.runPythonAsync(codeContent);
|
await currentPyodideInstance.runPythonAsync(codeContent);
|
||||||
|
|
||||||
const runOutput = await currentPyodideInstance.runPythonAsync(
|
|
||||||
`sys.stdout.getvalue()`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const runOutputByLines: string[] = runOutput.split('\n');
|
|
||||||
|
|
||||||
setConsoleOutputs((outputs) => [
|
setConsoleOutputs((outputs) => [
|
||||||
...outputs.filter((output) => output.id !== runId),
|
...outputs.filter((output) => output.id !== runId),
|
||||||
{
|
{
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
contents: runOutputByLines
|
contents: stdOutputs.filter((output) => output.value.trim().length),
|
||||||
.filter((line) => line.trim().length)
|
|
||||||
.map((line) => ({
|
|
||||||
type: line.startsWith('data:image/png;base64')
|
|
||||||
? 'image'
|
|
||||||
: 'text',
|
|
||||||
value: line,
|
|
||||||
})),
|
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -213,6 +169,8 @@ export function RunCodeButton({
|
||||||
try {
|
try {
|
||||||
pyodide.runPythonAsync(`
|
pyodide.runPythonAsync(`
|
||||||
import sys
|
import sys
|
||||||
|
import gc
|
||||||
|
|
||||||
has_plt = 'matplotlib.pyplot' in sys.modules
|
has_plt = 'matplotlib.pyplot' in sys.modules
|
||||||
|
|
||||||
if has_plt:
|
if has_plt:
|
||||||
|
|
@ -238,9 +196,15 @@ export function RunCodeButton({
|
||||||
loadAndRunPython();
|
loadAndRunPython();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={block.status === 'streaming'}
|
disabled={isBlockStreaming}
|
||||||
>
|
>
|
||||||
<PlayIcon size={18} /> Run
|
<PlayIcon size={18} /> Run
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const RunCodeButton = memo(PureRunCodeButton, (prevProps, nextProps) => {
|
||||||
|
if (prevProps.block.status !== nextProps.block.status) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue