Skip to content

Commit

Permalink
feat: support image outputs during code execution (vercel#674)
Browse files Browse the repository at this point in the history
Co-authored-by: Bailey Simrell <[email protected]>
  • Loading branch information
jeremyphilemon and BaileySimrell authored Jan 7, 2025
1 parent 50fbc0d commit 50bd460
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 117 deletions.
4 changes: 2 additions & 2 deletions app/(chat)/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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(),
Expand Down
106 changes: 4 additions & 102 deletions components/block-actions.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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,
Expand Down
9 changes: 7 additions & 2 deletions components/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
44 changes: 37 additions & 7 deletions components/console.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading

0 comments on commit 50bd460

Please sign in to comment.