chore: rename blocks to artifacts (#793)

This commit is contained in:
Jeremy 2025-02-13 08:25:57 -08:00 committed by GitHub
parent 01f589b603
commit 81f909ac3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 473 additions and 473 deletions

View file

@ -1,13 +1,13 @@
import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { blockDefinitions, UIBlock } from './block';
import { artifactDefinitions, UIArtifact } from './artifact';
import { Dispatch, memo, SetStateAction, useState } from 'react';
import { BlockActionContext } from './create-block';
import { ArtifactActionContext } from './create-artifact';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
interface BlockActionsProps {
block: UIBlock;
interface ArtifactActionsProps {
artifact: UIArtifact;
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
currentVersionIndex: number;
isCurrentVersion: boolean;
@ -16,27 +16,27 @@ interface BlockActionsProps {
setMetadata: Dispatch<SetStateAction<any>>;
}
function PureBlockActions({
block,
function PureArtifactActions({
artifact,
handleVersionChange,
currentVersionIndex,
isCurrentVersion,
mode,
metadata,
setMetadata,
}: BlockActionsProps) {
}: ArtifactActionsProps) {
const [isLoading, setIsLoading] = useState(false);
const blockDefinition = blockDefinitions.find(
(definition) => definition.kind === block.kind,
const artifactDefinition = artifactDefinitions.find(
(definition) => definition.kind === artifact.kind,
);
if (!blockDefinition) {
throw new Error('Block definition not found!');
if (!artifactDefinition) {
throw new Error('Artifact definition not found!');
}
const actionContext: BlockActionContext = {
content: block.content,
const actionContext: ArtifactActionContext = {
content: artifact.content,
handleVersionChange,
currentVersionIndex,
isCurrentVersion,
@ -47,7 +47,7 @@ function PureBlockActions({
return (
<div className="flex flex-row gap-1">
{blockDefinition.actions.map((action) => (
{artifactDefinition.actions.map((action) => (
<Tooltip key={action.description}>
<TooltipTrigger asChild>
<Button
@ -68,7 +68,7 @@ function PureBlockActions({
}
}}
disabled={
isLoading || block.status === 'streaming'
isLoading || artifact.status === 'streaming'
? true
: action.isDisabled
? action.isDisabled(actionContext)
@ -86,12 +86,15 @@ function PureBlockActions({
);
}
export const BlockActions = memo(PureBlockActions, (prevProps, nextProps) => {
if (prevProps.block.status !== nextProps.block.status) return false;
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
return false;
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
if (prevProps.block.content !== nextProps.block.content) return false;
export const ArtifactActions = memo(
PureArtifactActions,
(prevProps, nextProps) => {
if (prevProps.artifact.status !== nextProps.artifact.status) return false;
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
return false;
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
if (prevProps.artifact.content !== nextProps.artifact.content) return false;
return true;
});
return true;
},
);

View file

@ -0,0 +1,29 @@
import { memo } from 'react';
import { CrossIcon } from './icons';
import { Button } from './ui/button';
import { initialArtifactData, useArtifact } from '@/hooks/use-artifact';
function PureArtifactCloseButton() {
const { setArtifact } = useArtifact();
return (
<Button
variant="outline"
className="h-fit p-2 dark:hover:bg-zinc-700"
onClick={() => {
setArtifact((currentArtifact) =>
currentArtifact.status === 'streaming'
? {
...currentArtifact,
isVisible: false,
}
: { ...initialArtifactData, status: 'idle' },
);
}}
>
<CrossIcon size={18} />
</Button>
);
}
export const ArtifactCloseButton = memo(PureArtifactCloseButton, () => true);

View file

@ -4,9 +4,9 @@ import { Vote } from '@/lib/db/schema';
import { ChatRequestOptions, Message } from 'ai';
import { memo } from 'react';
import equal from 'fast-deep-equal';
import { UIBlock } from './block';
import { UIArtifact } from './artifact';
interface BlockMessagesProps {
interface ArtifactMessagesProps {
chatId: string;
isLoading: boolean;
votes: Array<Vote> | undefined;
@ -18,10 +18,10 @@ interface BlockMessagesProps {
chatRequestOptions?: ChatRequestOptions,
) => Promise<string | null | undefined>;
isReadonly: boolean;
blockStatus: UIBlock['status'];
artifactStatus: UIArtifact['status'];
}
function PureBlockMessages({
function PureArtifactMessages({
chatId,
isLoading,
votes,
@ -29,7 +29,7 @@ function PureBlockMessages({
setMessages,
reload,
isReadonly,
}: BlockMessagesProps) {
}: ArtifactMessagesProps) {
const [messagesContainerRef, messagesEndRef] =
useScrollToBottom<HTMLDivElement>();
@ -64,12 +64,12 @@ function PureBlockMessages({
}
function areEqual(
prevProps: BlockMessagesProps,
nextProps: BlockMessagesProps,
prevProps: ArtifactMessagesProps,
nextProps: ArtifactMessagesProps,
) {
if (
prevProps.blockStatus === 'streaming' &&
nextProps.blockStatus === 'streaming'
prevProps.artifactStatus === 'streaming' &&
nextProps.artifactStatus === 'streaming'
)
return true;
@ -81,4 +81,4 @@ function areEqual(
return true;
}
export const BlockMessages = memo(PureBlockMessages, areEqual);
export const ArtifactMessages = memo(PureArtifactMessages, areEqual);

View file

@ -21,24 +21,29 @@ import { fetcher } from '@/lib/utils';
import { MultimodalInput } from './multimodal-input';
import { Toolbar } from './toolbar';
import { VersionFooter } from './version-footer';
import { BlockActions } from './block-actions';
import { BlockCloseButton } from './block-close-button';
import { BlockMessages } from './block-messages';
import { ArtifactActions } from './artifact-actions';
import { ArtifactCloseButton } from './artifact-close-button';
import { ArtifactMessages } from './artifact-messages';
import { useSidebar } from './ui/sidebar';
import { useBlock } from '@/hooks/use-block';
import { imageBlock } from '@/blocks/image/client';
import { codeBlock } from '@/blocks/code/client';
import { sheetBlock } from '@/blocks/sheet/client';
import { textBlock } from '@/blocks/text/client';
import { useArtifact } from '@/hooks/use-artifact';
import { imageArtifact } from '@/artifacts/image/client';
import { codeArtifact } from '@/artifacts/code/client';
import { sheetArtifact } from '@/artifacts/sheet/client';
import { textArtifact } from '@/artifacts/text/client';
import equal from 'fast-deep-equal';
export const blockDefinitions = [textBlock, codeBlock, imageBlock, sheetBlock];
export type BlockKind = (typeof blockDefinitions)[number]['kind'];
export const artifactDefinitions = [
textArtifact,
codeArtifact,
imageArtifact,
sheetArtifact,
];
export type ArtifactKind = (typeof artifactDefinitions)[number]['kind'];
export interface UIBlock {
export interface UIArtifact {
title: string;
documentId: string;
kind: BlockKind;
kind: ArtifactKind;
content: string;
isVisible: boolean;
status: 'streaming' | 'idle';
@ -50,7 +55,7 @@ export interface UIBlock {
};
}
function PureBlock({
function PureArtifact({
chatId,
input,
setInput,
@ -91,15 +96,15 @@ function PureBlock({
) => Promise<string | null | undefined>;
isReadonly: boolean;
}) {
const { block, setBlock, metadata, setMetadata } = useBlock();
const { artifact, setArtifact, metadata, setMetadata } = useArtifact();
const {
data: documents,
isLoading: isDocumentsFetching,
mutate: mutateDocuments,
} = useSWR<Array<Document>>(
block.documentId !== 'init' && block.status !== 'streaming'
? `/api/document?id=${block.documentId}`
artifact.documentId !== 'init' && artifact.status !== 'streaming'
? `/api/document?id=${artifact.documentId}`
: null,
fetcher,
);
@ -117,27 +122,27 @@ function PureBlock({
if (mostRecentDocument) {
setDocument(mostRecentDocument);
setCurrentVersionIndex(documents.length - 1);
setBlock((currentBlock) => ({
...currentBlock,
setArtifact((currentArtifact) => ({
...currentArtifact,
content: mostRecentDocument.content ?? '',
}));
}
}
}, [documents, setBlock]);
}, [documents, setArtifact]);
useEffect(() => {
mutateDocuments();
}, [block.status, mutateDocuments]);
}, [artifact.status, mutateDocuments]);
const { mutate } = useSWRConfig();
const [isContentDirty, setIsContentDirty] = useState(false);
const handleContentChange = useCallback(
(updatedContent: string) => {
if (!block) return;
if (!artifact) return;
mutate<Array<Document>>(
`/api/document?id=${block.documentId}`,
`/api/document?id=${artifact.documentId}`,
async (currentDocuments) => {
if (!currentDocuments) return undefined;
@ -149,12 +154,12 @@ function PureBlock({
}
if (currentDocument.content !== updatedContent) {
await fetch(`/api/document?id=${block.documentId}`, {
await fetch(`/api/document?id=${artifact.documentId}`, {
method: 'POST',
body: JSON.stringify({
title: block.title,
title: artifact.title,
content: updatedContent,
kind: block.kind,
kind: artifact.kind,
}),
});
@ -173,7 +178,7 @@ function PureBlock({
{ revalidate: false },
);
},
[block, mutate],
[artifact, mutate],
);
const debouncedHandleContentChange = useDebounceCallback(
@ -241,28 +246,28 @@ function PureBlock({
const { width: windowWidth, height: windowHeight } = useWindowSize();
const isMobile = windowWidth ? windowWidth < 768 : false;
const blockDefinition = blockDefinitions.find(
(definition) => definition.kind === block.kind,
const artifactDefinition = artifactDefinitions.find(
(definition) => definition.kind === artifact.kind,
);
if (!blockDefinition) {
throw new Error('Block definition not found!');
if (!artifactDefinition) {
throw new Error('Artifact definition not found!');
}
useEffect(() => {
if (block.documentId !== 'init') {
if (blockDefinition.initialize) {
blockDefinition.initialize({
documentId: block.documentId,
if (artifact.documentId !== 'init') {
if (artifactDefinition.initialize) {
artifactDefinition.initialize({
documentId: artifact.documentId,
setMetadata,
});
}
}
}, [block.documentId, blockDefinition, setMetadata]);
}, [artifact.documentId, artifactDefinition, setMetadata]);
return (
<AnimatePresence>
{block.isVisible && (
{artifact.isVisible && (
<motion.div
className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-transparent"
initial={{ opacity: 1 }}
@ -318,7 +323,7 @@ function PureBlock({
</AnimatePresence>
<div className="flex flex-col h-full justify-between items-center gap-4">
<BlockMessages
<ArtifactMessages
chatId={chatId}
isLoading={isLoading}
votes={votes}
@ -326,7 +331,7 @@ function PureBlock({
setMessages={setMessages}
reload={reload}
isReadonly={isReadonly}
blockStatus={block.status}
artifactStatus={artifact.status}
/>
<form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
@ -355,18 +360,18 @@ function PureBlock({
isMobile
? {
opacity: 1,
x: block.boundingBox.left,
y: block.boundingBox.top,
height: block.boundingBox.height,
width: block.boundingBox.width,
x: artifact.boundingBox.left,
y: artifact.boundingBox.top,
height: artifact.boundingBox.height,
width: artifact.boundingBox.width,
borderRadius: 50,
}
: {
opacity: 1,
x: block.boundingBox.left,
y: block.boundingBox.top,
height: block.boundingBox.height,
width: block.boundingBox.width,
x: artifact.boundingBox.left,
y: artifact.boundingBox.top,
height: artifact.boundingBox.height,
width: artifact.boundingBox.width,
borderRadius: 50,
}
}
@ -418,10 +423,10 @@ function PureBlock({
>
<div className="p-2 flex flex-row justify-between items-start">
<div className="flex flex-row gap-4 items-start">
<BlockCloseButton />
<ArtifactCloseButton />
<div className="flex flex-col">
<div className="font-medium">{block.title}</div>
<div className="font-medium">{artifact.title}</div>
{isContentDirty ? (
<div className="text-sm text-muted-foreground">
@ -443,8 +448,8 @@ function PureBlock({
</div>
</div>
<BlockActions
block={block}
<ArtifactActions
artifact={artifact}
currentVersionIndex={currentVersionIndex}
handleVersionChange={handleVersionChange}
isCurrentVersion={isCurrentVersion}
@ -455,22 +460,22 @@ function PureBlock({
</div>
<div className="dark:bg-muted bg-background h-full overflow-y-scroll !max-w-full items-center">
<blockDefinition.content
title={block.title}
<artifactDefinition.content
title={artifact.title}
content={
isCurrentVersion
? block.content
? artifact.content
: getDocumentContentById(currentVersionIndex)
}
mode={mode}
status={block.status}
status={artifact.status}
currentVersionIndex={currentVersionIndex}
suggestions={[]}
onSaveContent={saveContent}
isInline={false}
isCurrentVersion={isCurrentVersion}
getDocumentContentById={getDocumentContentById}
isLoading={isDocumentsFetching && !block.content}
isLoading={isDocumentsFetching && !artifact.content}
metadata={metadata}
setMetadata={setMetadata}
/>
@ -484,7 +489,7 @@ function PureBlock({
isLoading={isLoading}
stop={stop}
setMessages={setMessages}
blockKind={block.kind}
artifactKind={artifact.kind}
/>
)}
</AnimatePresence>
@ -506,7 +511,7 @@ function PureBlock({
);
}
export const Block = memo(PureBlock, (prevProps, nextProps) => {
export const Artifact = memo(PureArtifact, (prevProps, nextProps) => {
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (!equal(prevProps.votes, nextProps.votes)) return false;
if (prevProps.input !== nextProps.input) return false;

View file

@ -1,29 +0,0 @@
import { memo } from 'react';
import { CrossIcon } from './icons';
import { Button } from './ui/button';
import { initialBlockData, useBlock } from '@/hooks/use-block';
function PureBlockCloseButton() {
const { setBlock } = useBlock();
return (
<Button
variant="outline"
className="h-fit p-2 dark:hover:bg-zinc-700"
onClick={() => {
setBlock((currentBlock) =>
currentBlock.status === 'streaming'
? {
...currentBlock,
isVisible: false,
}
: { ...initialBlockData, status: 'idle' },
);
}}
>
<CrossIcon size={18} />
</Button>
);
}
export const BlockCloseButton = memo(PureBlockCloseButton, () => true);

View file

@ -9,11 +9,11 @@ import { ChatHeader } from '@/components/chat-header';
import type { Vote } from '@/lib/db/schema';
import { fetcher, generateUUID } from '@/lib/utils';
import { Block } from './block';
import { Artifact } from './artifact';
import { MultimodalInput } from './multimodal-input';
import { Messages } from './messages';
import { VisibilityType } from './visibility-selector';
import { useBlockSelector } from '@/hooks/use-block';
import { useArtifactSelector } from '@/hooks/use-artifact';
import { toast } from 'sonner';
export function Chat({
@ -62,7 +62,7 @@ export function Chat({
);
const [attachments, setAttachments] = useState<Array<Attachment>>([]);
const isBlockVisible = useBlockSelector((state) => state.isVisible);
const isArtifactVisible = useArtifactSelector((state) => state.isVisible);
return (
<>
@ -82,7 +82,7 @@ export function Chat({
setMessages={setMessages}
reload={reload}
isReadonly={isReadonly}
isBlockVisible={isBlockVisible}
isArtifactVisible={isArtifactVisible}
/>
<form className="flex mx-auto px-4 bg-background pb-4 md:pb-6 gap-2 w-full md:max-w-3xl">
@ -104,7 +104,7 @@ export function Chat({
</form>
</div>
<Block
<Artifact
chatId={id}
input={input}
setInput={setInput}

View file

@ -1,11 +1,5 @@
'use client';
import { useCallback, useState } from 'react';
import { CodeIcon, LoaderIcon, PlayIcon, PythonIcon } from './icons';
import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { cn } from '@/lib/utils';
interface CodeBlockProps {
node: any;
inline: boolean;
@ -20,30 +14,15 @@ export function CodeBlock({
children,
...props
}: CodeBlockProps) {
const [output, setOutput] = useState<string | null>(null);
const [pyodide, setPyodide] = useState<any>(null);
const match = /language-(\w+)/.exec(className || '');
const isPython = match && match[1] === 'python';
const codeContent = String(children).replace(/\n$/, '');
const [tab, setTab] = useState<'code' | 'run'>('code');
if (!inline) {
return (
<div className="not-prose flex flex-col">
{tab === 'code' && (
<pre
{...props}
className={`text-sm w-full overflow-x-auto dark:bg-zinc-900 p-4 border border-zinc-200 dark:border-zinc-700 rounded-xl dark:text-zinc-50 text-zinc-900`}
>
<code className="whitespace-pre-wrap break-words">{children}</code>
</pre>
)}
{tab === 'run' && output && (
<div className="text-sm w-full overflow-x-auto bg-zinc-800 dark:bg-zinc-900 p-4 border border-zinc-200 dark:border-zinc-700 border-t-0 rounded-b-xl text-zinc-50">
<code>{output}</code>
</div>
)}
<pre
{...props}
className={`text-sm w-full overflow-x-auto dark:bg-zinc-900 p-4 border border-zinc-200 dark:border-zinc-700 rounded-xl dark:text-zinc-50 text-zinc-900`}
>
<code className="whitespace-pre-wrap break-words">{children}</code>
</pre>
</div>
);
} else {

View file

@ -9,7 +9,7 @@ import {
useState,
} from 'react';
import { cn } from '@/lib/utils';
import { useBlockSelector } from '@/hooks/use-block';
import { useArtifactSelector } from '@/hooks/use-artifact';
export interface ConsoleOutputContent {
type: 'text' | 'image';
@ -32,7 +32,7 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
const [isResizing, setIsResizing] = useState(false);
const consoleEndRef = useRef<HTMLDivElement>(null);
const isBlockVisible = useBlockSelector((state) => state.isVisible);
const isArtifactVisible = useArtifactSelector((state) => state.isVisible);
const minHeight = 100;
const maxHeight = 800;
@ -71,10 +71,10 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
}, [consoleOutputs]);
useEffect(() => {
if (!isBlockVisible) {
if (!isArtifactVisible) {
setConsoleOutputs([]);
}
}, [isBlockVisible, setConsoleOutputs]);
}, [isArtifactVisible, setConsoleOutputs]);
return consoleOutputs.length > 0 ? (
<>

View file

@ -2,9 +2,9 @@ import { Suggestion } from '@/lib/db/schema';
import { UseChatHelpers } from 'ai/react';
import { ComponentType, Dispatch, ReactNode, SetStateAction } from 'react';
import { DataStreamDelta } from './data-stream-handler';
import { UIBlock } from './block';
import { UIArtifact } from './artifact';
export type BlockActionContext<M = any> = {
export type ArtifactActionContext<M = any> = {
content: string;
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
currentVersionIndex: number;
@ -14,25 +14,25 @@ export type BlockActionContext<M = any> = {
setMetadata: Dispatch<SetStateAction<M>>;
};
type BlockAction<M = any> = {
type ArtifactAction<M = any> = {
icon: ReactNode;
label?: string;
description: string;
onClick: (context: BlockActionContext<M>) => Promise<void> | void;
isDisabled?: (context: BlockActionContext<M>) => boolean;
onClick: (context: ArtifactActionContext<M>) => Promise<void> | void;
isDisabled?: (context: ArtifactActionContext<M>) => boolean;
};
export type BlockToolbarContext = {
export type ArtifactToolbarContext = {
appendMessage: UseChatHelpers['append'];
};
export type BlockToolbarItem = {
export type ArtifactToolbarItem = {
description: string;
icon: ReactNode;
onClick: (context: BlockToolbarContext) => void;
onClick: (context: ArtifactToolbarContext) => void;
};
interface BlockContent<M = any> {
interface ArtifactContent<M = any> {
title: string;
content: string;
mode: 'edit' | 'diff';
@ -53,34 +53,34 @@ interface InitializeParameters<M = any> {
setMetadata: Dispatch<SetStateAction<M>>;
}
type BlockConfig<T extends string, M = any> = {
type ArtifactConfig<T extends string, M = any> = {
kind: T;
description: string;
content: ComponentType<BlockContent<M>>;
actions: Array<BlockAction<M>>;
toolbar: BlockToolbarItem[];
content: ComponentType<ArtifactContent<M>>;
actions: Array<ArtifactAction<M>>;
toolbar: ArtifactToolbarItem[];
initialize?: (parameters: InitializeParameters<M>) => void;
onStreamPart: (args: {
setMetadata: Dispatch<SetStateAction<M>>;
setBlock: Dispatch<SetStateAction<UIBlock>>;
setArtifact: Dispatch<SetStateAction<UIArtifact>>;
streamPart: DataStreamDelta;
}) => void;
};
export class Block<T extends string, M = any> {
export class Artifact<T extends string, M = any> {
readonly kind: T;
readonly description: string;
readonly content: ComponentType<BlockContent<M>>;
readonly actions: Array<BlockAction<M>>;
readonly toolbar: BlockToolbarItem[];
readonly content: ComponentType<ArtifactContent<M>>;
readonly actions: Array<ArtifactAction<M>>;
readonly toolbar: ArtifactToolbarItem[];
readonly initialize?: (parameters: InitializeParameters) => void;
readonly onStreamPart: (args: {
setMetadata: Dispatch<SetStateAction<M>>;
setBlock: Dispatch<SetStateAction<UIBlock>>;
setArtifact: Dispatch<SetStateAction<UIArtifact>>;
streamPart: DataStreamDelta;
}) => void;
constructor(config: BlockConfig<T, M>) {
constructor(config: ArtifactConfig<T, M>) {
this.kind = config.kind;
this.description = config.description;
this.content = config.content;

View file

@ -2,9 +2,9 @@
import { useChat } from 'ai/react';
import { useEffect, useRef } from 'react';
import { blockDefinitions, BlockKind } from './block';
import { artifactDefinitions, ArtifactKind } from './artifact';
import { Suggestion } from '@/lib/db/schema';
import { initialBlockData, useBlock } from '@/hooks/use-block';
import { initialArtifactData, useArtifact } from '@/hooks/use-artifact';
export type DataStreamDelta = {
type:
@ -23,7 +23,7 @@ export type DataStreamDelta = {
export function DataStreamHandler({ id }: { id: string }) {
const { data: dataStream } = useChat({ id });
const { block, setBlock, setMetadata } = useBlock();
const { artifact, setArtifact, setMetadata } = useArtifact();
const lastProcessedIndex = useRef(-1);
useEffect(() => {
@ -33,64 +33,64 @@ export function DataStreamHandler({ id }: { id: string }) {
lastProcessedIndex.current = dataStream.length - 1;
(newDeltas as DataStreamDelta[]).forEach((delta: DataStreamDelta) => {
const blockDefinition = blockDefinitions.find(
(blockDefinition) => blockDefinition.kind === block.kind,
const artifactDefinition = artifactDefinitions.find(
(artifactDefinition) => artifactDefinition.kind === artifact.kind,
);
if (blockDefinition?.onStreamPart) {
blockDefinition.onStreamPart({
if (artifactDefinition?.onStreamPart) {
artifactDefinition.onStreamPart({
streamPart: delta,
setBlock,
setArtifact,
setMetadata,
});
}
setBlock((draftBlock) => {
if (!draftBlock) {
return { ...initialBlockData, status: 'streaming' };
setArtifact((draftArtifact) => {
if (!draftArtifact) {
return { ...initialArtifactData, status: 'streaming' };
}
switch (delta.type) {
case 'id':
return {
...draftBlock,
...draftArtifact,
documentId: delta.content as string,
status: 'streaming',
};
case 'title':
return {
...draftBlock,
...draftArtifact,
title: delta.content as string,
status: 'streaming',
};
case 'kind':
return {
...draftBlock,
kind: delta.content as BlockKind,
...draftArtifact,
kind: delta.content as ArtifactKind,
status: 'streaming',
};
case 'clear':
return {
...draftBlock,
...draftArtifact,
content: '',
status: 'streaming',
};
case 'finish':
return {
...draftBlock,
...draftArtifact,
status: 'idle',
};
default:
return draftBlock;
return draftArtifact;
}
});
});
}, [dataStream, setBlock, setMetadata, block]);
}, [dataStream, setArtifact, setMetadata, artifact]);
return null;
}

View file

@ -8,16 +8,16 @@ import {
useMemo,
useRef,
} from 'react';
import { BlockKind, UIBlock } from './block';
import { ArtifactKind, UIArtifact } from './artifact';
import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from './icons';
import { cn, fetcher } from '@/lib/utils';
import { Document } from '@/lib/db/schema';
import { InlineDocumentSkeleton } from './document-skeleton';
import useSWR from 'swr';
import { Editor } from './editor';
import { Editor } from './text-editor';
import { DocumentToolCall, DocumentToolResult } from './document';
import { CodeEditor } from './code-editor';
import { useBlock } from '@/hooks/use-block';
import { useArtifact } from '@/hooks/use-artifact';
import equal from 'fast-deep-equal';
import { SpreadsheetEditor } from './sheet-editor';
import { ImageEditor } from './image-editor';
@ -33,7 +33,7 @@ export function DocumentPreview({
result,
args,
}: DocumentPreviewProps) {
const { block, setBlock } = useBlock();
const { artifact, setArtifact } = useArtifact();
const { data: documents, isLoading: isDocumentsFetching } = useSWR<
Array<Document>
@ -45,9 +45,9 @@ export function DocumentPreview({
useEffect(() => {
const boundingBox = hitboxRef.current?.getBoundingClientRect();
if (block.documentId && boundingBox) {
setBlock((block) => ({
...block,
if (artifact.documentId && boundingBox) {
setArtifact((artifact) => ({
...artifact,
boundingBox: {
left: boundingBox.x,
top: boundingBox.y,
@ -56,9 +56,9 @@ export function DocumentPreview({
},
}));
}
}, [block.documentId, setBlock]);
}, [artifact.documentId, setArtifact]);
if (block.isVisible) {
if (artifact.isVisible) {
if (result) {
return (
<DocumentToolResult
@ -81,38 +81,42 @@ export function DocumentPreview({
}
if (isDocumentsFetching) {
return <LoadingSkeleton blockKind={result.kind ?? args.kind} />;
return <LoadingSkeleton artifactKind={result.kind ?? args.kind} />;
}
const document: Document | null = previewDocument
? previewDocument
: block.status === 'streaming'
: artifact.status === 'streaming'
? {
title: block.title,
kind: block.kind,
content: block.content,
id: block.documentId,
title: artifact.title,
kind: artifact.kind,
content: artifact.content,
id: artifact.documentId,
createdAt: new Date(),
userId: 'noop',
}
: null;
if (!document) return <LoadingSkeleton blockKind={block.kind} />;
if (!document) return <LoadingSkeleton artifactKind={artifact.kind} />;
return (
<div className="relative w-full cursor-pointer">
<HitboxLayer hitboxRef={hitboxRef} result={result} setBlock={setBlock} />
<HitboxLayer
hitboxRef={hitboxRef}
result={result}
setArtifact={setArtifact}
/>
<DocumentHeader
title={document.title}
kind={document.kind}
isStreaming={block.status === 'streaming'}
isStreaming={artifact.status === 'streaming'}
/>
<DocumentContent document={document} />
</div>
);
}
const LoadingSkeleton = ({ blockKind }: { blockKind: BlockKind }) => (
const LoadingSkeleton = ({ artifactKind }: { artifactKind: ArtifactKind }) => (
<div className="w-full">
<div className="p-4 border rounded-t-2xl flex flex-row gap-2 items-center justify-between dark:bg-muted h-[57px] dark:border-zinc-700 border-b-0">
<div className="flex flex-row items-center gap-3">
@ -125,7 +129,7 @@ const LoadingSkeleton = ({ blockKind }: { blockKind: BlockKind }) => (
<FullscreenIcon />
</div>
</div>
{blockKind === 'image' ? (
{artifactKind === 'image' ? (
<div className="overflow-y-scroll border rounded-b-2xl bg-muted border-t-0 dark:border-zinc-700">
<div className="animate-pulse h-[257px] bg-muted-foreground/20 w-full" />
</div>
@ -140,21 +144,23 @@ const LoadingSkeleton = ({ blockKind }: { blockKind: BlockKind }) => (
const PureHitboxLayer = ({
hitboxRef,
result,
setBlock,
setArtifact,
}: {
hitboxRef: React.RefObject<HTMLDivElement>;
result: any;
setBlock: (updaterFn: UIBlock | ((currentBlock: UIBlock) => UIBlock)) => void;
setArtifact: (
updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact),
) => void;
}) => {
const handleClick = useCallback(
(event: MouseEvent<HTMLElement>) => {
const boundingBox = event.currentTarget.getBoundingClientRect();
setBlock((block) =>
block.status === 'streaming'
? { ...block, isVisible: true }
setArtifact((artifact) =>
artifact.status === 'streaming'
? { ...artifact, isVisible: true }
: {
...block,
...artifact,
title: result.title,
documentId: result.id,
kind: result.kind,
@ -168,7 +174,7 @@ const PureHitboxLayer = ({
},
);
},
[setBlock, result],
[setArtifact, result],
);
return (
@ -199,7 +205,7 @@ const PureDocumentHeader = ({
isStreaming,
}: {
title: string;
kind: BlockKind;
kind: ArtifactKind;
isStreaming: boolean;
}) => (
<div className="p-4 border rounded-t-2xl flex flex-row gap-2 items-start sm:items-center justify-between dark:bg-muted border-b-0 dark:border-zinc-700">
@ -229,7 +235,7 @@ const DocumentHeader = memo(PureDocumentHeader, (prevProps, nextProps) => {
});
const DocumentContent = ({ document }: { document: Document }) => {
const { block } = useBlock();
const { artifact } = useArtifact();
const containerClassName = cn(
'h-[257px] overflow-y-scroll border rounded-b-2xl dark:bg-muted border-t-0 dark:border-zinc-700',
@ -243,7 +249,7 @@ const DocumentContent = ({ document }: { document: Document }) => {
content: document.content ?? '',
isCurrentVersion: true,
currentVersionIndex: 0,
status: block.status,
status: artifact.status,
saveContent: () => {},
suggestions: [],
};
@ -270,7 +276,7 @@ const DocumentContent = ({ document }: { document: Document }) => {
content={document.content ?? ''}
isCurrentVersion={true}
currentVersionIndex={0}
status={block.status}
status={artifact.status}
isInline={true}
/>
) : null}

View file

@ -1,9 +1,13 @@
'use client';
import { BlockKind } from './block';
import { ArtifactKind } from './artifact';
export const DocumentSkeleton = ({ blockKind }: { blockKind: BlockKind }) => {
return blockKind === 'image' ? (
export const DocumentSkeleton = ({
artifactKind,
}: {
artifactKind: ArtifactKind;
}) => {
return artifactKind === 'image' ? (
<div className="flex flex-col gap-4 w-full justify-center items-center h-[calc(100dvh-60px)]">
<div className="animate-pulse rounded-lg bg-muted-foreground/20 size-96" />
</div>

View file

@ -1,9 +1,9 @@
import { memo } from 'react';
import type { BlockKind } from './block';
import type { ArtifactKind } from './artifact';
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
import { toast } from 'sonner';
import { useBlock } from '@/hooks/use-block';
import { useArtifact } from '@/hooks/use-artifact';
const getActionText = (
type: 'create' | 'update' | 'request-suggestions',
@ -25,7 +25,7 @@ const getActionText = (
interface DocumentToolResultProps {
type: 'create' | 'update' | 'request-suggestions';
result: { id: string; title: string; kind: BlockKind };
result: { id: string; title: string; kind: ArtifactKind };
isReadonly: boolean;
}
@ -34,7 +34,7 @@ function PureDocumentToolResult({
result,
isReadonly,
}: DocumentToolResultProps) {
const { setBlock } = useBlock();
const { setArtifact } = useArtifact();
return (
<button
@ -57,7 +57,7 @@ function PureDocumentToolResult({
height: rect.height,
};
setBlock({
setArtifact({
documentId: result.id,
kind: result.kind,
content: '',
@ -97,7 +97,7 @@ function PureDocumentToolCall({
args,
isReadonly,
}: DocumentToolCallProps) {
const { setBlock } = useBlock();
const { setArtifact } = useArtifact();
return (
<button
@ -120,8 +120,8 @@ function PureDocumentToolCall({
height: rect.height,
};
setBlock((currentBlock) => ({
...currentBlock,
setArtifact((currentArtifact) => ({
...currentArtifact,
isVisible: true,
boundingBox,
}));

View file

@ -18,7 +18,7 @@ interface MessagesProps {
chatRequestOptions?: ChatRequestOptions,
) => Promise<string | null | undefined>;
isReadonly: boolean;
isBlockVisible: boolean;
isArtifactVisible: boolean;
}
function PureMessages({
@ -70,7 +70,7 @@ function PureMessages({
}
export const Messages = memo(PureMessages, (prevProps, nextProps) => {
if (prevProps.isBlockVisible && nextProps.isBlockVisible) return true;
if (prevProps.isArtifactVisible && nextProps.isArtifactVisible) return true;
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.isLoading && nextProps.isLoading) return false;

View file

@ -9,16 +9,16 @@ import type { UISuggestion } from '@/lib/editor/suggestions';
import { CrossIcon, MessageIcon } from './icons';
import { Button } from './ui/button';
import { cn } from '@/lib/utils';
import { BlockKind } from './block';
import { ArtifactKind } from './artifact';
export const Suggestion = ({
suggestion,
onApply,
blockKind,
artifactKind,
}: {
suggestion: UISuggestion;
onApply: () => void;
blockKind: BlockKind;
artifactKind: ArtifactKind;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const { width: windowWidth } = useWindowSize();
@ -28,8 +28,8 @@ export const Suggestion = ({
{!isExpanded ? (
<motion.div
className={cn('cursor-pointer text-muted-foreground p-1', {
'absolute -right-8': blockKind === 'text',
'sticky top-0 right-4': blockKind === 'code',
'absolute -right-8': artifactKind === 'text',
'sticky top-0 right-4': artifactKind === 'code',
})}
onClick={() => {
setIsExpanded(true);

View file

@ -37,8 +37,8 @@ import {
StopIcon,
SummarizeIcon,
} from './icons';
import { blockDefinitions, BlockKind } from './block';
import { BlockToolbarItem } from './create-block';
import { artifactDefinitions, ArtifactKind } from './artifact';
import { ArtifactToolbarItem } from './create-artifact';
import { UseChatHelpers } from 'ai/react';
type ToolProps = {
@ -273,7 +273,7 @@ export const Tools = ({
) => Promise<string | null | undefined>;
isAnimating: boolean;
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
tools: Array<BlockToolbarItem>;
tools: Array<ArtifactToolbarItem>;
}) => {
const [primaryTool, ...secondaryTools] = tools;
@ -322,7 +322,7 @@ const PureToolbar = ({
isLoading,
stop,
setMessages,
blockKind,
artifactKind,
}: {
isToolbarVisible: boolean;
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
@ -333,7 +333,7 @@ const PureToolbar = ({
) => Promise<string | null | undefined>;
stop: () => void;
setMessages: Dispatch<SetStateAction<Message[]>>;
blockKind: BlockKind;
artifactKind: ArtifactKind;
}) => {
const toolbarRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
@ -377,17 +377,17 @@ const PureToolbar = ({
}
}, [isLoading, setIsToolbarVisible]);
const blockDefinition = blockDefinitions.find(
(definition) => definition.kind === blockKind,
const artifactDefinition = artifactDefinitions.find(
(definition) => definition.kind === artifactKind,
);
if (!blockDefinition) {
throw new Error('Block definition not found!');
if (!artifactDefinition) {
throw new Error('Artifact definition not found!');
}
const toolsByBlockKind = blockDefinition.toolbar;
const toolsByArtifactKind = artifactDefinition.toolbar;
if (toolsByBlockKind.length === 0) {
if (toolsByArtifactKind.length === 0) {
return null;
}
@ -409,7 +409,7 @@ const PureToolbar = ({
: {
opacity: 1,
y: 0,
height: toolsByBlockKind.length * 50,
height: toolsByArtifactKind.length * 50,
transition: { delay: 0 },
scale: 1,
}
@ -466,7 +466,7 @@ const PureToolbar = ({
selectedTool={selectedTool}
setIsToolbarVisible={setIsToolbarVisible}
setSelectedTool={setSelectedTool}
tools={toolsByBlockKind}
tools={toolsByArtifactKind}
/>
)}
</motion.div>
@ -477,7 +477,7 @@ const PureToolbar = ({
export const Toolbar = memo(PureToolbar, (prevProps, nextProps) => {
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.isToolbarVisible !== nextProps.isToolbarVisible) return false;
if (prevProps.blockKind !== nextProps.blockKind) return false;
if (prevProps.artifactKind !== nextProps.artifactKind) return false;
return true;
});

View file

@ -9,10 +9,9 @@ import { useWindowSize } from 'usehooks-ts';
import type { Document } from '@/lib/db/schema';
import { getDocumentTimestampByIndex } from '@/lib/utils';
import type { UIBlock } from './block';
import { LoaderIcon } from './icons';
import { Button } from './ui/button';
import { useBlock } from '@/hooks/use-block';
import { useArtifact } from '@/hooks/use-artifact';
interface VersionFooterProps {
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
@ -25,7 +24,7 @@ export const VersionFooter = ({
documents,
currentVersionIndex,
}: VersionFooterProps) => {
const { block } = useBlock();
const { artifact } = useArtifact();
const { width } = useWindowSize();
const isMobile = width < 768;
@ -57,8 +56,8 @@ export const VersionFooter = ({
setIsMutating(true);
mutate(
`/api/document?id=${block.documentId}`,
await fetch(`/api/document?id=${block.documentId}`, {
`/api/document?id=${artifact.documentId}`,
await fetch(`/api/document?id=${artifact.documentId}`, {
method: 'PATCH',
body: JSON.stringify({
timestamp: getDocumentTimestampByIndex(