fix: prevent empty code execution outputs after remount (#678)

This commit is contained in:
Jeremy 2025-01-08 16:37:49 +05:30 committed by GitHub
parent 1df0181035
commit 3e34d678b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 85 deletions

View file

@ -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>

View file

@ -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) => {
setConsoleOutputs((outputs) => [
// Detect and load required packages ...outputs.filter((output) => output.id !== runId),
const requiredPackages = detectPythonImports( {
codeContent, id: runId,
currentPyodideInstance, contents: [{ type: 'text', value: message }],
); status: 'loading_packages',
},
if (requiredPackages.size > 0) { ]);
setConsoleOutputs((outputs) => [ },
...outputs.filter((output) => output.id !== runId), });
{
id: runId,
contents: [],
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;
});