chore: rename blocks to artifacts (#793)
This commit is contained in:
parent
01f589b603
commit
81f909ac3a
41 changed files with 473 additions and 473 deletions
|
|
@ -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;
|
||||
},
|
||||
);
|
||||
29
components/artifact-close-button.tsx
Normal file
29
components/artifact-close-button.tsx
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue