diff --git a/blocks/code.tsx b/blocks/code.tsx index 681e9c2c9..ca285c15d 100644 --- a/blocks/code.tsx +++ b/blocks/code.tsx @@ -10,7 +10,57 @@ import { } from '@/components/icons'; import { toast } from 'sonner'; import { generateUUID } from '@/lib/utils'; -import { Console, ConsoleOutput } from '@/components/console'; +import { + Console, + ConsoleOutput, + ConsoleOutputContent, +} from '@/components/console'; + +const OUTPUT_HANDLERS = { + matplotlib: ` + import io + import base64 + 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; +} interface Metadata { outputs: Array; @@ -20,9 +70,11 @@ export const codeBlock = new Block<'code', Metadata>({ kind: 'code', description: 'Useful for code generation; Code execution is only available for python code.', - initialize: () => ({ - outputs: [], - }), + initialize: async ({ setMetadata }) => { + setMetadata({ + outputs: [], + }); + }, onStreamPart: ({ streamPart, setBlock }) => { if (streamPart.type === 'code-delta') { setBlock((draftBlock) => ({ @@ -41,7 +93,9 @@ export const codeBlock = new Block<'code', Metadata>({ content: ({ metadata, setMetadata, ...props }) => { return ( <> - +
+ +
{metadata?.outputs && ( ({ description: 'Execute code', onClick: async ({ content, setMetadata }) => { const runId = generateUUID(); - const outputs: any[] = []; - - // @ts-expect-error - loadPyodide is not defined - const currentPyodideInstance = await globalThis.loadPyodide({ - indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', - }); + const outputContent: Array = []; - currentPyodideInstance.setStdout({ - batched: (output: string) => { - outputs.push({ + setMetadata((metadata) => ({ + ...metadata, + outputs: [ + ...metadata.outputs, + { id: runId, - contents: [ - { - type: output.startsWith('data:image/png;base64') - ? 'image' - : 'text', - value: output, - }, - ], - status: 'completed', - }); - }, - }); + contents: [], + status: 'in_progress', + }, + ], + })); - await currentPyodideInstance.loadPackagesFromImports(content, { - messageCallback: (message: string) => { - outputs.push({ - id: runId, - contents: [{ type: 'text', value: message }], - status: 'loading_packages', - }); - }, - }); + try { + // @ts-expect-error - loadPyodide is not defined + const currentPyodideInstance = await globalThis.loadPyodide({ + indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', + }); - await currentPyodideInstance.runPythonAsync(content); + currentPyodideInstance.setStdout({ + batched: (output: string) => { + outputContent.push({ + type: output.startsWith('data:image/png;base64') + ? 'image' + : 'text', + value: output, + }); + }, + }); - setMetadata((metadata: any) => ({ - ...metadata, - outputs, - })); + await currentPyodideInstance.loadPackagesFromImports(content, { + messageCallback: (message: string) => { + setMetadata((metadata) => ({ + ...metadata, + outputs: [ + ...metadata.outputs.filter((output) => output.id !== runId), + { + id: runId, + contents: [{ type: 'text', value: message }], + status: 'loading_packages', + }, + ], + })); + }, + }); + + const requiredHandlers = detectRequiredHandlers(content); + 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(content); + + setMetadata((metadata) => ({ + ...metadata, + outputs: [ + ...metadata.outputs.filter((output) => output.id !== runId), + { + id: runId, + contents: outputContent, + status: 'completed', + }, + ], + })); + } catch (error: any) { + setMetadata((metadata) => ({ + ...metadata, + outputs: [ + ...metadata.outputs.filter((output) => output.id !== runId), + { + id: runId, + contents: [{ type: 'text', value: error.message }], + status: 'failed', + }, + ], + })); + } }, }, { @@ -112,6 +214,13 @@ export const codeBlock = new Block<'code', Metadata>({ onClick: ({ handleVersionChange }) => { handleVersionChange('prev'); }, + isDisabled: ({ currentVersionIndex }) => { + if (currentVersionIndex === 0) { + return true; + } + + return false; + }, }, { icon: , @@ -119,6 +228,13 @@ export const codeBlock = new Block<'code', Metadata>({ onClick: ({ handleVersionChange }) => { handleVersionChange('next'); }, + isDisabled: ({ isCurrentVersion }) => { + if (isCurrentVersion) { + return true; + } + + return false; + }, }, { icon: , diff --git a/blocks/image.tsx b/blocks/image.tsx index 3bdf1aa78..96c82a917 100644 --- a/blocks/image.tsx +++ b/blocks/image.tsx @@ -1,6 +1,7 @@ import { Block } from '@/components/create-block'; import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons'; import { ImageEditor } from '@/components/image-editor'; +import { toast } from 'sonner'; export const imageBlock = new Block({ kind: 'image', @@ -23,6 +24,13 @@ export const imageBlock = new Block({ onClick: ({ handleVersionChange }) => { handleVersionChange('prev'); }, + isDisabled: ({ currentVersionIndex }) => { + if (currentVersionIndex === 0) { + return true; + } + + return false; + }, }, { icon: , @@ -30,6 +38,13 @@ export const imageBlock = new Block({ onClick: ({ handleVersionChange }) => { handleVersionChange('next'); }, + isDisabled: ({ isCurrentVersion }) => { + if (isCurrentVersion) { + return true; + } + + return false; + }, }, { icon: , @@ -52,6 +67,8 @@ export const imageBlock = new Block({ } }, 'image/png'); }; + + toast.success('Copied image to clipboard!'); }, }, ], diff --git a/blocks/text.tsx b/blocks/text.tsx index ad3739dad..7678d7a2b 100644 --- a/blocks/text.tsx +++ b/blocks/text.tsx @@ -80,18 +80,22 @@ export const textBlock = new Block<'text', TextBlockMetadata>({ return ( <> - +
+ - {metadata && metadata.suggestions && metadata.suggestions.length > 0 ? ( -
- ) : null} + {metadata && + metadata.suggestions && + metadata.suggestions.length > 0 ? ( +
+ ) : null} +
); }, diff --git a/components/block-actions.tsx b/components/block-actions.tsx index d355f35be..b660d704f 100644 --- a/components/block-actions.tsx +++ b/components/block-actions.tsx @@ -1,9 +1,10 @@ import { Button } from './ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { blockDefinitions, UIBlock } from './block'; -import { Dispatch, memo, SetStateAction } from 'react'; +import { Dispatch, memo, SetStateAction, useState } from 'react'; import { BlockActionContext } from './create-block'; import { cn } from '@/lib/utils'; +import { toast } from 'sonner'; interface BlockActionsProps { block: UIBlock; @@ -24,6 +25,8 @@ function PureBlockActions({ metadata, setMetadata, }: BlockActionsProps) { + const [isLoading, setIsLoading] = useState(false); + const blockDefinition = blockDefinitions.find( (definition) => definition.kind === block.kind, ); @@ -53,9 +56,23 @@ function PureBlockActions({ 'p-2': !action.label, 'py-1.5 px-2': action.label, })} - onClick={() => action.onClick(actionContext)} + onClick={async () => { + setIsLoading(true); + + try { + await Promise.resolve(action.onClick(actionContext)); + } catch (error) { + toast.error('Failed to execute action'); + } finally { + setIsLoading(false); + } + }} disabled={ - action.isDisabled ? action.isDisabled(actionContext) : false + isLoading || block.status === 'streaming' + ? true + : action.isDisabled + ? action.isDisabled(actionContext) + : false } > {action.icon} diff --git a/components/block.tsx b/components/block.tsx index 6e38704f8..3892a565e 100644 --- a/components/block.tsx +++ b/components/block.tsx @@ -17,7 +17,7 @@ import { import useSWR, { useSWRConfig } from 'swr'; import { useDebounceCallback, useWindowSize } from 'usehooks-ts'; import type { Document, Vote } from '@/lib/db/schema'; -import { cn, fetcher } from '@/lib/utils'; +import { fetcher } from '@/lib/utils'; import { MultimodalInput } from './multimodal-input'; import { Toolbar } from './toolbar'; import { VersionFooter } from './version-footer'; @@ -26,10 +26,10 @@ import { BlockCloseButton } from './block-close-button'; import { BlockMessages } from './block-messages'; import { useSidebar } from './ui/sidebar'; import { useBlock } from '@/hooks/use-block'; -import equal from 'fast-deep-equal'; import { textBlock } from '@/blocks/text'; import { imageBlock } from '@/blocks/image'; import { codeBlock } from '@/blocks/code'; +import equal from 'fast-deep-equal'; export const blockDefinitions = [textBlock, codeBlock, imageBlock] as const; export type BlockKind = (typeof blockDefinitions)[number]['kind']; @@ -250,10 +250,12 @@ function PureBlock({ useEffect(() => { if (block && block.documentId !== 'init') { - blockDefinition.initialize({ - documentId: block.documentId, - setMetadata, - }); + if (blockDefinition.initialize) { + blockDefinition.initialize({ + documentId: block.documentId, + setMetadata, + }); + } } }, [block, blockDefinition, setMetadata]); @@ -451,55 +453,40 @@ function PureBlock({ />
-
-
- +
+ - - {isCurrentVersion && ( - - )} - -
+ + {isCurrentVersion && ( + + )} +
diff --git a/components/create-block.tsx b/components/create-block.tsx index 542b8a1dd..b0e7d79c7 100644 --- a/components/create-block.tsx +++ b/components/create-block.tsx @@ -4,22 +4,22 @@ import { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react'; import { DataStreamDelta } from './data-stream-handler'; import { UIBlock } from './block'; -export type BlockActionContext = { +export type BlockActionContext = { content: string; handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; currentVersionIndex: number; isCurrentVersion: boolean; mode: 'edit' | 'diff'; - metadata: any; - setMetadata: Dispatch>; + metadata: M; + setMetadata: Dispatch>; }; -type BlockAction = { +type BlockAction = { icon: ReactNode; label?: string; description: string; - onClick: (context: BlockActionContext) => void; - isDisabled?: (context: BlockActionContext) => boolean; + onClick: (context: BlockActionContext) => Promise | void; + isDisabled?: (context: BlockActionContext) => boolean; }; export type BlockToolbarContext = { @@ -32,7 +32,7 @@ export type BlockToolbarItem = { onClick: (context: BlockToolbarContext) => void; }; -type BlockContent = { +interface BlockContent { title: string; content: string; mode: 'edit' | 'diff'; @@ -44,9 +44,9 @@ type BlockContent = { isInline: boolean; getDocumentContentById: (index: number) => string; isLoading: boolean; - metadata: any; - setMetadata: Dispatch>; -}; + metadata: M; + setMetadata: Dispatch>; +} interface InitializeParameters { documentId: string; @@ -56,17 +56,11 @@ interface InitializeParameters { type BlockConfig = { kind: T; description: string; - content: ComponentType< - Omit & { - metadata: M; - setMetadata: Dispatch>; - } - >; - actions?: BlockAction[]; - toolbar?: BlockToolbarItem[]; - metadata?: M; + content: ComponentType>; + actions: Array>; + toolbar: BlockToolbarItem[]; initialize?: (parameters: InitializeParameters) => void; - onStreamPart?: (args: { + onStreamPart: (args: { setMetadata: Dispatch>; setBlock: Dispatch>; streamPart: DataStreamDelta; @@ -76,12 +70,11 @@ type BlockConfig = { export class Block { readonly kind: T; readonly description: string; - readonly content: ComponentType; - readonly actions: BlockAction[]; + readonly content: ComponentType>; + readonly actions: Array>; readonly toolbar: BlockToolbarItem[]; - readonly metadata: M; - readonly initialize: (parameters: InitializeParameters) => void; - readonly onStreamPart?: (args: { + readonly initialize?: (parameters: InitializeParameters) => void; + readonly onStreamPart: (args: { setMetadata: Dispatch>; setBlock: Dispatch>; streamPart: DataStreamDelta; @@ -93,7 +86,6 @@ export class Block { this.content = config.content; this.actions = config.actions || []; this.toolbar = config.toolbar || []; - this.metadata = config.metadata as M; this.initialize = config.initialize || (async () => ({})); this.onStreamPart = config.onStreamPart; }