feat: support image outputs during code execution (#674)

Co-authored-by: Bailey Simrell <baileysimrell@gmail.com>
This commit is contained in:
Jeremy
2025-01-08 00:00:21 +05:30
committed by GitHub
parent 50fbc0dab2
commit 50bd4604b6
7 changed files with 313 additions and 117 deletions
+2 -2
View File
@@ -254,7 +254,7 @@ export async function POST(request: Request) {
if (document.kind === 'text') {
const { fullStream } = streamText({
model: customModel(model.apiIdentifier),
system: updateDocumentPrompt(currentContent),
system: updateDocumentPrompt(currentContent, 'text'),
prompt: description,
experimental_providerMetadata: {
openai: {
@@ -284,7 +284,7 @@ export async function POST(request: Request) {
} else if (document.kind === 'code') {
const { fullStream } = streamObject({
model: customModel(model.apiIdentifier),
system: updateDocumentPrompt(currentContent),
system: updateDocumentPrompt(currentContent, 'code'),
prompt: description,
schema: z.object({
code: z.string(),
+4 -102
View File
@@ -1,18 +1,12 @@
import { cn, generateUUID } from '@/lib/utils';
import { ClockRewind, CopyIcon, PlayIcon, RedoIcon, UndoIcon } from './icons';
import { cn } from '@/lib/utils';
import { ClockRewind, CopyIcon, RedoIcon, UndoIcon } from './icons';
import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { useCopyToClipboard } from 'usehooks-ts';
import { toast } from 'sonner';
import { ConsoleOutput, UIBlock } from './block';
import {
Dispatch,
memo,
SetStateAction,
startTransition,
useCallback,
useState,
} from 'react';
import { Dispatch, memo, SetStateAction } from 'react';
import { RunCodeButton } from './run-code-button';
interface BlockActionsProps {
block: UIBlock;
@@ -23,98 +17,6 @@ interface BlockActionsProps {
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
}
export function RunCodeButton({
block,
setConsoleOutputs,
}: {
block: UIBlock;
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
}) {
const [pyodide, setPyodide] = useState<any>(null);
const isPython = true;
const codeContent = block.content;
const updateConsoleOutput = useCallback(
(runId: string, content: string | null, status: 'completed' | 'failed') => {
setConsoleOutputs((consoleOutputs) => {
const index = consoleOutputs.findIndex((output) => output.id === runId);
if (index === -1) return consoleOutputs;
const updatedOutputs = [...consoleOutputs];
updatedOutputs[index] = {
id: runId,
content,
status,
};
return updatedOutputs;
});
},
[setConsoleOutputs],
);
const loadAndRunPython = useCallback(async () => {
const runId = generateUUID();
setConsoleOutputs((consoleOutputs) => [
...consoleOutputs,
{
id: runId,
content: null,
status: 'in_progress',
},
]);
let currentPyodideInstance = pyodide;
if (isPython) {
if (!currentPyodideInstance) {
// @ts-expect-error - pyodide is not defined
const newPyodideInstance = await loadPyodide({
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
});
setPyodide(newPyodideInstance);
currentPyodideInstance = newPyodideInstance;
}
try {
await currentPyodideInstance.runPythonAsync(`
import sys
import io
sys.stdout = io.StringIO()
`);
await currentPyodideInstance.runPythonAsync(codeContent);
const output: string = await currentPyodideInstance.runPythonAsync(
`sys.stdout.getvalue()`,
);
updateConsoleOutput(runId, output, 'completed');
} catch (error: any) {
updateConsoleOutput(runId, error.message, 'failed');
}
}
}, [pyodide, codeContent, isPython, setConsoleOutputs, updateConsoleOutput]);
return (
<Button
variant="outline"
className="py-1.5 px-2 h-fit dark:hover:bg-zinc-700"
onClick={() => {
startTransition(() => {
loadAndRunPython();
});
}}
disabled={block.status === 'streaming'}
>
<PlayIcon size={18} /> Run
</Button>
);
}
function PureBlockActions({
block,
handleVersionChange,
+7 -2
View File
@@ -52,10 +52,15 @@ export interface UIBlock {
};
}
export interface ConsoleOutputContent {
type: 'text' | 'image';
value: string;
}
export interface ConsoleOutput {
id: string;
status: 'in_progress' | 'completed' | 'failed';
content: string | null;
status: 'in_progress' | 'loading_packages' | 'completed' | 'failed';
contents: Array<ConsoleOutputContent>;
}
function PureBlock({
+37 -7
View File
@@ -101,21 +101,51 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
>
<div
className={cn('w-12 shrink-0', {
'text-muted-foreground':
consoleOutput.status === 'in_progress',
'text-muted-foreground': [
'in_progress',
'loading_packages',
].includes(consoleOutput.status),
'text-emerald-500': consoleOutput.status === 'completed',
'text-red-400': consoleOutput.status === 'failed',
})}
>
[{index + 1}]
</div>
{consoleOutput.status === 'in_progress' ? (
<div className="animate-spin size-fit self-center">
<LoaderIcon />
{['in_progress', 'loading_packages'].includes(
consoleOutput.status,
) ? (
<div className="flex flex-row gap-2">
<div className="animate-spin size-fit self-center">
<LoaderIcon />
</div>
<div className="text-muted-foreground">
{consoleOutput.status === 'in_progress'
? 'Initializing...'
: consoleOutput.status === 'loading_packages'
? 'Loading Packages...'
: null}
</div>
</div>
) : (
<div className="dark:text-zinc-50 text-zinc-900 whitespace-pre-line">
{consoleOutput.content}
<div className="dark:text-zinc-50 text-zinc-900 w-full flex flex-col gap-2">
{consoleOutput.contents.map((content, index) =>
content.type === 'image' ? (
<picture key={`${consoleOutput.id}-${index}`}>
<img
src={content.value}
alt="output"
className="rounded-md max-w-[600px] w-full"
/>
</picture>
) : (
<div
key={`${consoleOutput.id}-${index}`}
className="whitespace-pre-line"
>
{content.value}
</div>
),
)}
</div>
)}
</div>
+246
View File
@@ -0,0 +1,246 @@
import { generateUUID } from '@/lib/utils';
import {
type Dispatch,
type SetStateAction,
startTransition,
useCallback,
useState,
useEffect,
} from 'react';
import type { ConsoleOutput, UIBlock } from './block';
import { Button } from './ui/button';
import { PlayIcon } from './icons';
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 = {
matplotlib: `
from matplotlib import pyplot as plt
# Clear any existing plots
plt.clf()
plt.close('all')
# Switch to agg backend
plt.switch_backend('agg')
def setup_matplotlib_output():
def custom_show():
if plt.gcf().get_size_inches().prod() * plt.gcf().dpi ** 2 > 25_000_000:
print("Warning: Plot size too large, reducing quality")
plt.gcf().set_dpi(100)
png_buf = io.BytesIO()
plt.savefig(png_buf, format='png')
png_buf.seek(0)
png_base64 = base64.b64encode(png_buf.read()).decode('utf-8')
print(f'data:image/png;base64,{png_base64}')
png_buf.close()
plt.clf()
plt.close('all')
plt.show = custom_show
`,
basic: `
# Basic output capture setup
`,
};
function detectRequiredHandlers(code: string): string[] {
const handlers: string[] = ['basic'];
if (code.includes('matplotlib') || code.includes('plt.')) {
handlers.push('matplotlib');
}
return handlers;
}
export function RunCodeButton({
block,
setConsoleOutputs,
}: {
block: UIBlock;
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
}) {
const [pyodide, setPyodide] = useState<any>(null);
const isPython = true;
const codeContent = block.content;
const loadAndRunPython = useCallback(async () => {
const runId = generateUUID();
setConsoleOutputs((outputs) => [
...outputs,
{
id: runId,
contents: [],
status: 'in_progress',
},
]);
let currentPyodideInstance = pyodide;
if (isPython) {
try {
if (!currentPyodideInstance) {
// @ts-expect-error - loadPyodide is not defined
const newPyodideInstance = await loadPyodide({
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
});
setPyodide(newPyodideInstance);
currentPyodideInstance = newPyodideInstance;
}
await currentPyodideInstance.runPythonAsync(`
import sys, io, gc
import base64
sys.stdout = io.StringIO()
`);
// Detect and load required packages
const requiredPackages = detectPythonImports(
codeContent,
currentPyodideInstance,
);
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);
for (const handler of requiredHandlers) {
if (OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS]) {
await currentPyodideInstance.runPythonAsync(
OUTPUT_HANDLERS[handler as keyof typeof OUTPUT_HANDLERS],
);
if (handler === 'matplotlib') {
await currentPyodideInstance.runPythonAsync(
'setup_matplotlib_output()',
);
}
}
}
await currentPyodideInstance.runPythonAsync(codeContent);
const runOutput = await currentPyodideInstance.runPythonAsync(
`sys.stdout.getvalue()`,
);
const runOutputByLines: string[] = runOutput.split('\n');
setConsoleOutputs((outputs) => [
...outputs.filter((output) => output.id !== runId),
{
id: generateUUID(),
contents: runOutputByLines
.filter((line) => line.trim().length)
.map((line) => ({
type: line.startsWith('data:image/png;base64')
? 'image'
: 'text',
value: line,
})),
status: 'completed',
},
]);
} catch (error: any) {
setConsoleOutputs((outputs) => [
...outputs.filter((output) => output.id !== runId),
{
id: runId,
contents: [{ type: 'text', value: error.message }],
status: 'failed',
},
]);
}
}
}, [pyodide, codeContent, isPython, setConsoleOutputs]);
useEffect(() => {
return () => {
if (pyodide) {
try {
pyodide.runPythonAsync(`
import sys
has_plt = 'matplotlib.pyplot' in sys.modules
if has_plt:
import matplotlib.pyplot as plt
plt.clf()
plt.close('all')
gc.collect()
`);
} catch (error) {
console.warn('Cleanup failed:', error);
}
}
};
}, [pyodide]);
return (
<Button
variant="outline"
className="py-1.5 px-2 h-fit dark:hover:bg-zinc-700"
onClick={() => {
startTransition(() => {
loadAndRunPython();
});
}}
disabled={block.status === 'streaming'}
>
<PlayIcon size={18} /> Run
</Button>
);
}
+17 -3
View File
@@ -1,3 +1,5 @@
import { BlockKind } from '@/components/block';
export const blocksPrompt = `
Blocks is a special user interface mode that helps users with writing, editing, and other content creation tasks. When block is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the blocks and visible to the user.
@@ -62,8 +64,20 @@ print(f"Factorial of 5 is: {factorial(5)}")
\`\`\`
`;
export const updateDocumentPrompt = (currentContent: string | null) => `\
Update the following contents of the document based on the given prompt.
export const updateDocumentPrompt = (
currentContent: string | null,
type: BlockKind,
) =>
type === 'text'
? `\
Improve the following contents of the document based on the given prompt.
${currentContent}
`;
`
: type === 'code'
? `\
Improve the following code snippet based on the given prompt.
${currentContent}
`
: '';
-1
View File
@@ -1,7 +1,6 @@
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */
experimental: {
ppr: true,
},