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,5 +1,5 @@
|
|||
import { auth } from '@/app/(auth)/auth';
|
||||
import { BlockKind } from '@/components/block';
|
||||
import { ArtifactKind } from '@/components/artifact';
|
||||
import {
|
||||
deleteDocumentsByIdAfterTimestamp,
|
||||
getDocumentsById,
|
||||
|
|
@ -53,7 +53,8 @@ export async function POST(request: Request) {
|
|||
content,
|
||||
title,
|
||||
kind,
|
||||
}: { content: string; title: string; kind: BlockKind } = await request.json();
|
||||
}: { content: string; title: string; kind: ArtifactKind } =
|
||||
await request.json();
|
||||
|
||||
if (session.user?.id) {
|
||||
const document = await saveDocument({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Block } from '@/components/create-block';
|
||||
import { Artifact } from '@/components/create-artifact';
|
||||
import { CodeEditor } from '@/components/code-editor';
|
||||
import {
|
||||
CopyIcon,
|
||||
|
|
@ -66,7 +66,7 @@ interface Metadata {
|
|||
outputs: Array<ConsoleOutput>;
|
||||
}
|
||||
|
||||
export const codeBlock = new Block<'code', Metadata>({
|
||||
export const codeArtifact = new Artifact<'code', Metadata>({
|
||||
kind: 'code',
|
||||
description:
|
||||
'Useful for code generation; Code execution is only available for python code.',
|
||||
|
|
@ -75,17 +75,17 @@ export const codeBlock = new Block<'code', Metadata>({
|
|||
outputs: [],
|
||||
});
|
||||
},
|
||||
onStreamPart: ({ streamPart, setBlock }) => {
|
||||
onStreamPart: ({ streamPart, setArtifact }) => {
|
||||
if (streamPart.type === 'code-delta') {
|
||||
setBlock((draftBlock) => ({
|
||||
...draftBlock,
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.content as string,
|
||||
isVisible:
|
||||
draftBlock.status === 'streaming' &&
|
||||
draftBlock.content.length > 300 &&
|
||||
draftBlock.content.length < 310
|
||||
draftArtifact.status === 'streaming' &&
|
||||
draftArtifact.content.length > 300 &&
|
||||
draftArtifact.content.length < 310
|
||||
? true
|
||||
: draftBlock.isVisible,
|
||||
: draftArtifact.isVisible,
|
||||
status: 'streaming',
|
||||
}));
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||
import { streamObject } from 'ai';
|
||||
import { myProvider } from '@/lib/ai/models';
|
||||
import { codePrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
import { createDocumentHandler } from '@/lib/blocks/server';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
|
||||
export const codeDocumentHandler = createDocumentHandler<'code'>({
|
||||
kind: 'code',
|
||||
|
|
@ -10,7 +10,7 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: codePrompt,
|
||||
prompt: title,
|
||||
schema: z.object({
|
||||
|
|
@ -42,7 +42,7 @@ export const codeDocumentHandler = createDocumentHandler<'code'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifacts-model'),
|
||||
system: updateDocumentPrompt(document.content, 'code'),
|
||||
prompt: description,
|
||||
schema: z.object({
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Block } from '@/components/create-block';
|
||||
import { Artifact } from '@/components/create-artifact';
|
||||
import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons';
|
||||
import { ImageEditor } from '@/components/image-editor';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export const imageBlock = new Block({
|
||||
export const imageArtifact = new Artifact({
|
||||
kind: 'image',
|
||||
description: 'Useful for image generation',
|
||||
onStreamPart: ({ streamPart, setBlock }) => {
|
||||
onStreamPart: ({ streamPart, setArtifact }) => {
|
||||
if (streamPart.type === 'image-delta') {
|
||||
setBlock((draftBlock) => ({
|
||||
...draftBlock,
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.content as string,
|
||||
isVisible: true,
|
||||
status: 'streaming',
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { myProvider } from '@/lib/ai/models';
|
||||
import { createDocumentHandler } from '@/lib/blocks/server';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { experimental_generateImage } from 'ai';
|
||||
|
||||
export const imageDocumentHandler = createDocumentHandler<'image'>({
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Block } from '@/components/create-block';
|
||||
import { Artifact } from '@/components/create-artifact';
|
||||
import {
|
||||
CopyIcon,
|
||||
LineChartIcon,
|
||||
|
|
@ -12,14 +12,14 @@ import { toast } from 'sonner';
|
|||
|
||||
type Metadata = any;
|
||||
|
||||
export const sheetBlock = new Block<'sheet', Metadata>({
|
||||
export const sheetArtifact = new Artifact<'sheet', Metadata>({
|
||||
kind: 'sheet',
|
||||
description: 'Useful for working with spreadsheets',
|
||||
initialize: async () => {},
|
||||
onStreamPart: ({ setBlock, streamPart }) => {
|
||||
onStreamPart: ({ setArtifact, streamPart }) => {
|
||||
if (streamPart.type === 'sheet-delta') {
|
||||
setBlock((draftBlock) => ({
|
||||
...draftBlock,
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: streamPart.content as string,
|
||||
isVisible: true,
|
||||
status: 'streaming',
|
||||
|
|
@ -107,7 +107,7 @@ export const sheetBlock = new Block<'sheet', Metadata>({
|
|||
appendMessage({
|
||||
role: 'user',
|
||||
content:
|
||||
'Can you please analyze and visualize the data by creating a new code block in python?',
|
||||
'Can you please analyze and visualize the data by creating a new code artifact in python?',
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { myProvider } from '@/lib/ai/models';
|
||||
import { sheetPrompt, updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
import { createDocumentHandler } from '@/lib/blocks/server';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { streamObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: sheetPrompt,
|
||||
prompt: title,
|
||||
schema: z.object({
|
||||
|
|
@ -47,7 +47,7 @@ export const sheetDocumentHandler = createDocumentHandler<'sheet'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamObject({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: updateDocumentPrompt(document.content, 'sheet'),
|
||||
prompt: description,
|
||||
schema: z.object({
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Block } from '@/components/create-block';
|
||||
import { Artifact } from '@/components/create-artifact';
|
||||
import { DiffView } from '@/components/diffview';
|
||||
import { DocumentSkeleton } from '@/components/document-skeleton';
|
||||
import { Editor } from '@/components/editor';
|
||||
import { Editor } from '@/components/text-editor';
|
||||
import {
|
||||
ClockRewind,
|
||||
CopyIcon,
|
||||
|
|
@ -14,11 +14,11 @@ import { Suggestion } from '@/lib/db/schema';
|
|||
import { toast } from 'sonner';
|
||||
import { getSuggestions } from '../actions';
|
||||
|
||||
interface TextBlockMetadata {
|
||||
interface TextArtifactMetadata {
|
||||
suggestions: Array<Suggestion>;
|
||||
}
|
||||
|
||||
export const textBlock = new Block<'text', TextBlockMetadata>({
|
||||
export const textArtifact = new Artifact<'text', TextArtifactMetadata>({
|
||||
kind: 'text',
|
||||
description: 'Useful for text content, like drafting essays and emails.',
|
||||
initialize: async ({ documentId, setMetadata }) => {
|
||||
|
|
@ -28,7 +28,7 @@ export const textBlock = new Block<'text', TextBlockMetadata>({
|
|||
suggestions,
|
||||
});
|
||||
},
|
||||
onStreamPart: ({ streamPart, setMetadata, setBlock }) => {
|
||||
onStreamPart: ({ streamPart, setMetadata, setArtifact }) => {
|
||||
if (streamPart.type === 'suggestion') {
|
||||
setMetadata((metadata) => {
|
||||
return {
|
||||
|
|
@ -41,16 +41,16 @@ export const textBlock = new Block<'text', TextBlockMetadata>({
|
|||
}
|
||||
|
||||
if (streamPart.type === 'text-delta') {
|
||||
setBlock((draftBlock) => {
|
||||
setArtifact((draftArtifact) => {
|
||||
return {
|
||||
...draftBlock,
|
||||
content: draftBlock.content + (streamPart.content as string),
|
||||
...draftArtifact,
|
||||
content: draftArtifact.content + (streamPart.content as string),
|
||||
isVisible:
|
||||
draftBlock.status === 'streaming' &&
|
||||
draftBlock.content.length > 400 &&
|
||||
draftBlock.content.length < 450
|
||||
draftArtifact.status === 'streaming' &&
|
||||
draftArtifact.content.length > 400 &&
|
||||
draftArtifact.content.length < 450
|
||||
? true
|
||||
: draftBlock.isVisible,
|
||||
: draftArtifact.isVisible,
|
||||
status: 'streaming',
|
||||
};
|
||||
});
|
||||
|
|
@ -68,7 +68,7 @@ export const textBlock = new Block<'text', TextBlockMetadata>({
|
|||
metadata,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return <DocumentSkeleton blockKind="text" />;
|
||||
return <DocumentSkeleton artifactKind="text" />;
|
||||
}
|
||||
|
||||
if (mode === 'diff') {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { smoothStream, streamText } from 'ai';
|
||||
import { myProvider } from '@/lib/ai/models';
|
||||
import { createDocumentHandler } from '@/lib/blocks/server';
|
||||
import { createDocumentHandler } from '@/lib/artifacts/server';
|
||||
import { updateDocumentPrompt } from '@/lib/ai/prompts';
|
||||
|
||||
export const textDocumentHandler = createDocumentHandler<'text'>({
|
||||
|
|
@ -9,7 +9,7 @@ export const textDocumentHandler = createDocumentHandler<'text'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system:
|
||||
'Write about the given topic. Markdown is supported. Use headings wherever appropriate.',
|
||||
experimental_transform: smoothStream({ chunking: 'word' }),
|
||||
|
|
@ -37,7 +37,7 @@ export const textDocumentHandler = createDocumentHandler<'text'>({
|
|||
let draftContent = '';
|
||||
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system: updateDocumentPrompt(document.content, 'text'),
|
||||
experimental_transform: smoothStream({ chunking: 'word' }),
|
||||
prompt: description,
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const myProvider = customProvider({
|
|||
middleware: extractReasoningMiddleware({ tagName: "think" }),
|
||||
}),
|
||||
"title-model": openai("gpt-4-turbo"),
|
||||
"block-model": openai("gpt-4o-mini"),
|
||||
"artifact-model": openai("gpt-4o-mini"),
|
||||
},
|
||||
imageModels: {
|
||||
"small-model": openai.image("dall-e-3"),
|
||||
|
|
@ -42,7 +42,7 @@ export const myProvider = customProvider({
|
|||
middleware: extractReasoningMiddleware({ tagName: "think" }),
|
||||
}),
|
||||
"title-model": openai("gpt-4-turbo"),
|
||||
"block-model": openai("gpt-4o-mini"),
|
||||
"artifact-model": openai("gpt-4o-mini"),
|
||||
},
|
||||
imageModels: {
|
||||
"small-model": openai.image("dall-e-3"),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
# Blocks
|
||||
# Artifacts
|
||||
|
||||
Blocks is a special user interface mode that allows you to have a workspace like interface along with the chat interface. This is similar to [ChatGPT's Canvas](https://openai.com/index/introducing-canvas) and [Claude's Artifacts](https://www.anthropic.com/news/artifacts).
|
||||
Artifacts is a special user interface mode that allows you to have a workspace like interface along with the chat interface. This is similar to [ChatGPT's Canvas](https://openai.com/index/introducing-canvas) and [Claude's Artifacts](https://www.anthropic.com/news/artifacts).
|
||||
|
||||
The template already ships with the following blocks:
|
||||
The template already ships with the following artifacts:
|
||||
|
||||
- **Text Block**: A block that allows you to work with text content like drafting essays and emails.
|
||||
- **Code Block**: A block that allows you to write and execute code (Python).
|
||||
- **Image Block**: A block that allows you to work with images like editing, annotating, and processing images.
|
||||
- **Sheet Block**: A block that allows you to work with tabular data like creating, editing, and analyzing data.
|
||||
- **Text Artifact**: A artifact that allows you to work with text content like drafting essays and emails.
|
||||
- **Code Artifact**: A artifact that allows you to write and execute code (Python).
|
||||
- **Image Artifact**: A artifact that allows you to work with images like editing, annotating, and processing images.
|
||||
- **Sheet Artifact**: A artifact that allows you to work with tabular data like creating, editing, and analyzing data.
|
||||
|
||||
## Adding a Custom Block
|
||||
## Adding a Custom Artifact
|
||||
|
||||
To add a custom block, you will need to create a folder in the `blocks` directory with the block name. The folder should contain the following files:
|
||||
To add a custom artifact, you will need to create a folder in the `artifacts` directory with the artifact name. The folder should contain the following files:
|
||||
|
||||
- `client.tsx`: The client-side code for the block.
|
||||
- `server.ts`: The server-side code for the block.
|
||||
- `client.tsx`: The client-side code for the artifact.
|
||||
- `server.ts`: The server-side code for the artifact.
|
||||
|
||||
Here is an example of a custom block called `CustomBlock`:
|
||||
Here is an example of a custom artifact called `CustomArtifact`:
|
||||
|
||||
```bash
|
||||
blocks/
|
||||
artifacts/
|
||||
custom/
|
||||
client.tsx
|
||||
server.ts
|
||||
|
|
@ -27,30 +27,30 @@ blocks/
|
|||
|
||||
### Client-Side Example (client.tsx)
|
||||
|
||||
This file is responsible for rendering your custom block. You might replace the inner UI with your own components, but the overall pattern (initialization, handling streamed data, and rendering content) remains the same. For instance:
|
||||
This file is responsible for rendering your custom artifact. You might replace the inner UI with your own components, but the overall pattern (initialization, handling streamed data, and rendering content) remains the same. For instance:
|
||||
|
||||
```tsx
|
||||
import { Block } from "@/components/create-block";
|
||||
import { Artifact } from "@/components/create-artifact";
|
||||
import { ExampleComponent } from "@/components/example-component";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface CustomBlockMetadata {
|
||||
// Define metadata your custom block might need—the example below is minimal.
|
||||
interface CustomArtifactMetadata {
|
||||
// Define metadata your custom artifact might need—the example below is minimal.
|
||||
info: string;
|
||||
}
|
||||
|
||||
export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
||||
export const customArtifact = new Artifact<"custom", CustomArtifactMetadata>({
|
||||
kind: "custom",
|
||||
description: "A custom block for demonstrating custom functionality.",
|
||||
description: "A custom artifact for demonstrating custom functionality.",
|
||||
// Initialization can fetch any extra data or perform side effects
|
||||
initialize: async ({ documentId, setMetadata }) => {
|
||||
// For example, initialize the block with default metadata.
|
||||
// For example, initialize the artifact with default metadata.
|
||||
setMetadata({
|
||||
info: `Document ${documentId} initialized.`,
|
||||
});
|
||||
},
|
||||
// Handle streamed parts from the server (if your block supports streaming updates)
|
||||
onStreamPart: ({ streamPart, setMetadata, setBlock }) => {
|
||||
// Handle streamed parts from the server (if your artifact supports streaming updates)
|
||||
onStreamPart: ({ streamPart, setMetadata, setArtifact }) => {
|
||||
if (streamPart.type === "info-update") {
|
||||
setMetadata((metadata) => ({
|
||||
...metadata,
|
||||
|
|
@ -58,14 +58,14 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
}));
|
||||
}
|
||||
if (streamPart.type === "content-update") {
|
||||
setBlock((draftBlock) => ({
|
||||
...draftBlock,
|
||||
content: draftBlock.content + (streamPart.content as string),
|
||||
setArtifact((draftArtifact) => ({
|
||||
...draftArtifact,
|
||||
content: draftArtifact.content + (streamPart.content as string),
|
||||
status: "streaming",
|
||||
}));
|
||||
}
|
||||
},
|
||||
// Defines how the block content is rendered
|
||||
// Defines how the artifact content is rendered
|
||||
content: ({
|
||||
mode,
|
||||
status,
|
||||
|
|
@ -78,7 +78,7 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
metadata,
|
||||
}) => {
|
||||
if (isLoading) {
|
||||
return <div>Loading custom block...</div>;
|
||||
return <div>Loading custom artifact...</div>;
|
||||
}
|
||||
|
||||
if (mode === "diff") {
|
||||
|
|
@ -94,7 +94,7 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="custom-block">
|
||||
<div className="custom-artifact">
|
||||
<ExampleComponent
|
||||
content={content}
|
||||
metadata={metadata}
|
||||
|
|
@ -112,15 +112,15 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
</div>
|
||||
);
|
||||
},
|
||||
// An optional set of actions exposed in the block toolbar.
|
||||
// An optional set of actions exposed in the artifact toolbar.
|
||||
actions: [
|
||||
{
|
||||
icon: <span>⟳</span>,
|
||||
description: "Refresh block info",
|
||||
description: "Refresh artifact info",
|
||||
onClick: ({ appendMessage }) => {
|
||||
appendMessage({
|
||||
role: "user",
|
||||
content: "Please refresh the info for my custom block.",
|
||||
content: "Please refresh the info for my custom artifact.",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -129,11 +129,11 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
toolbar: [
|
||||
{
|
||||
icon: <span>✎</span>,
|
||||
description: "Edit custom block",
|
||||
description: "Edit custom artifact",
|
||||
onClick: ({ appendMessage }) => {
|
||||
appendMessage({
|
||||
role: "user",
|
||||
content: "Edit the custom block content.",
|
||||
content: "Edit the custom artifact content.",
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
@ -143,12 +143,12 @@ export const customBlock = new Block<"custom", CustomBlockMetadata>({
|
|||
|
||||
Server-Side Example (server.ts)
|
||||
|
||||
The server file processes the document for the block. It streams updates (if applicable) and returns the final content. For example:
|
||||
The server file processes the document for the artifact. It streams updates (if applicable) and returns the final content. For example:
|
||||
|
||||
```ts
|
||||
import { smoothStream, streamText } from "ai";
|
||||
import { myProvider } from "@/lib/ai/models";
|
||||
import { createDocumentHandler } from "@/lib/blocks/server";
|
||||
import { createDocumentHandler } from "@/lib/artifacts/server";
|
||||
import { updateDocumentPrompt } from "@/lib/ai/prompts";
|
||||
|
||||
export const customDocumentHandler = createDocumentHandler<"custom">({
|
||||
|
|
@ -158,7 +158,7 @@ export const customDocumentHandler = createDocumentHandler<"custom">({
|
|||
let draftContent = "";
|
||||
// For demonstration, use streamText to generate content.
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel("block-model"),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system:
|
||||
"Generate a creative piece based on the title. Markdown is supported.",
|
||||
experimental_transform: smoothStream({ chunking: "word" }),
|
||||
|
|
@ -182,7 +182,7 @@ export const customDocumentHandler = createDocumentHandler<"custom">({
|
|||
onUpdateDocument: async ({ document, description, dataStream }) => {
|
||||
let draftContent = "";
|
||||
const { fullStream } = streamText({
|
||||
model: myProvider.languageModel("block-model"),
|
||||
model: myProvider.languageModel("artifact-model"),
|
||||
system: updateDocumentPrompt(document.content, "custom"),
|
||||
experimental_transform: smoothStream({ chunking: "word" }),
|
||||
prompt: description,
|
||||
|
|
@ -211,15 +211,15 @@ export const customDocumentHandler = createDocumentHandler<"custom">({
|
|||
});
|
||||
```
|
||||
|
||||
Once you have created the client and server files, you can import the block in the `lib/blocks/server.ts` file and add it to the `documentHandlersByBlockKind` array.
|
||||
Once you have created the client and server files, you can import the artifact in the `lib/artifacts/server.ts` file and add it to the `documentHandlersByArtifactKind` array.
|
||||
|
||||
```ts
|
||||
export const documentHandlersByBlockKind: Array<DocumentHandler> = [
|
||||
export const documentHandlersByArtifactKind: Array<DocumentHandler> = [
|
||||
...,
|
||||
customDocumentHandler,
|
||||
];
|
||||
|
||||
export const blockKinds = [..., "custom"] as const;
|
||||
export const artifactKinds = [..., "custom"] as const;
|
||||
```
|
||||
|
||||
Specify it in document schema at `lib/db/schema.ts`.
|
||||
|
|
@ -232,7 +232,7 @@ export const document = pgTable(
|
|||
createdAt: timestamp("createdAt").notNull(),
|
||||
title: text("title").notNull(),
|
||||
content: text("content"),
|
||||
kind: varchar("text", { enum: [..., "custom"] }) // Add the custom block kind here
|
||||
kind: varchar("text", { enum: [..., "custom"] }) // Add the custom artifact kind here
|
||||
.notNull()
|
||||
.default("text"),
|
||||
userId: uuid("userId")
|
||||
|
|
@ -247,12 +247,12 @@ export const document = pgTable(
|
|||
);
|
||||
```
|
||||
|
||||
And also add the client-side block to the `blockDefinitions` array in the `components/block.tsx` file.
|
||||
And also add the client-side artifact to the `artifactDefinitions` array in the `components/artifact.tsx` file.
|
||||
|
||||
```ts
|
||||
import { customBlock } from "@/blocks/custom/client";
|
||||
import { customArtifact } from "@/artifacts/custom/client";
|
||||
|
||||
export const blockDefinitions = [..., customBlock];
|
||||
export const artifactDefinitions = [..., customArtifact];
|
||||
```
|
||||
|
||||
You should now be able to see the custom block in the workspace!
|
||||
You should now be able to see the custom artifact in the workspace!
|
||||
|
|
|
|||
85
hooks/use-artifact.ts
Normal file
85
hooks/use-artifact.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { UIArtifact } from '@/components/artifact';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const initialArtifactData: UIArtifact = {
|
||||
documentId: 'init',
|
||||
content: '',
|
||||
kind: 'text',
|
||||
title: '',
|
||||
status: 'idle',
|
||||
isVisible: false,
|
||||
boundingBox: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type Selector<T> = (state: UIArtifact) => T;
|
||||
|
||||
export function useArtifactSelector<Selected>(selector: Selector<Selected>) {
|
||||
const { data: localArtifact } = useSWR<UIArtifact>('artifact', null, {
|
||||
fallbackData: initialArtifactData,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (!localArtifact) return selector(initialArtifactData);
|
||||
return selector(localArtifact);
|
||||
}, [localArtifact, selector]);
|
||||
|
||||
return selectedValue;
|
||||
}
|
||||
|
||||
export function useArtifact() {
|
||||
const { data: localArtifact, mutate: setLocalArtifact } = useSWR<UIArtifact>(
|
||||
'artifact',
|
||||
null,
|
||||
{
|
||||
fallbackData: initialArtifactData,
|
||||
},
|
||||
);
|
||||
|
||||
const artifact = useMemo(() => {
|
||||
if (!localArtifact) return initialArtifactData;
|
||||
return localArtifact;
|
||||
}, [localArtifact]);
|
||||
|
||||
const setArtifact = useCallback(
|
||||
(updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact)) => {
|
||||
setLocalArtifact((currentArtifact) => {
|
||||
const artifactToUpdate = currentArtifact || initialArtifactData;
|
||||
|
||||
if (typeof updaterFn === 'function') {
|
||||
return updaterFn(artifactToUpdate);
|
||||
}
|
||||
|
||||
return updaterFn;
|
||||
});
|
||||
},
|
||||
[setLocalArtifact],
|
||||
);
|
||||
|
||||
const { data: localArtifactMetadata, mutate: setLocalArtifactMetadata } =
|
||||
useSWR<any>(
|
||||
() =>
|
||||
artifact.documentId ? `artifact-metadata-${artifact.documentId}` : null,
|
||||
null,
|
||||
{
|
||||
fallbackData: null,
|
||||
},
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
artifact,
|
||||
setArtifact,
|
||||
metadata: localArtifactMetadata,
|
||||
setMetadata: setLocalArtifactMetadata,
|
||||
}),
|
||||
[artifact, setArtifact, localArtifactMetadata, setLocalArtifactMetadata],
|
||||
);
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import useSWR from 'swr';
|
||||
import { UIBlock } from '@/components/block';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export const initialBlockData: UIBlock = {
|
||||
documentId: 'init',
|
||||
content: '',
|
||||
kind: 'text',
|
||||
title: '',
|
||||
status: 'idle',
|
||||
isVisible: false,
|
||||
boundingBox: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type Selector<T> = (state: UIBlock) => T;
|
||||
|
||||
export function useBlockSelector<Selected>(selector: Selector<Selected>) {
|
||||
const { data: localBlock } = useSWR<UIBlock>('block', null, {
|
||||
fallbackData: initialBlockData,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (!localBlock) return selector(initialBlockData);
|
||||
return selector(localBlock);
|
||||
}, [localBlock, selector]);
|
||||
|
||||
return selectedValue;
|
||||
}
|
||||
|
||||
export function useBlock() {
|
||||
const { data: localBlock, mutate: setLocalBlock } = useSWR<UIBlock>(
|
||||
'block',
|
||||
null,
|
||||
{
|
||||
fallbackData: initialBlockData,
|
||||
},
|
||||
);
|
||||
|
||||
const block = useMemo(() => {
|
||||
if (!localBlock) return initialBlockData;
|
||||
return localBlock;
|
||||
}, [localBlock]);
|
||||
|
||||
const setBlock = useCallback(
|
||||
(updaterFn: UIBlock | ((currentBlock: UIBlock) => UIBlock)) => {
|
||||
setLocalBlock((currentBlock) => {
|
||||
const blockToUpdate = currentBlock || initialBlockData;
|
||||
|
||||
if (typeof updaterFn === 'function') {
|
||||
return updaterFn(blockToUpdate);
|
||||
}
|
||||
|
||||
return updaterFn;
|
||||
});
|
||||
},
|
||||
[setLocalBlock],
|
||||
);
|
||||
|
||||
const { data: localBlockMetadata, mutate: setLocalBlockMetadata } =
|
||||
useSWR<any>(
|
||||
() => (block.documentId ? `block-metadata-${block.documentId}` : null),
|
||||
null,
|
||||
{
|
||||
fallbackData: null,
|
||||
},
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
block,
|
||||
setBlock,
|
||||
metadata: localBlockMetadata,
|
||||
setMetadata: setLocalBlockMetadata,
|
||||
}),
|
||||
[block, setBlock, localBlockMetadata, setLocalBlockMetadata],
|
||||
);
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ export const myProvider = customProvider({
|
|||
middleware: extractReasoningMiddleware({ tagName: 'think' }),
|
||||
}),
|
||||
'title-model': openai('gpt-4-turbo'),
|
||||
'block-model': openai('gpt-4o-mini'),
|
||||
'artifact-model': openai('gpt-4o-mini'),
|
||||
},
|
||||
imageModels: {
|
||||
'small-model': openai.image('dall-e-2'),
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { BlockKind } from '@/components/block';
|
||||
import { ArtifactKind } from '@/components/artifact';
|
||||
|
||||
export const blocksPrompt = `
|
||||
Blocks is a special user interface mode that helps users with writing, editing, and other content creation tasks. When block is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the blocks and visible to the user.
|
||||
export const artifactsPrompt = `
|
||||
Artifacts is a special user interface mode that helps users with writing, editing, and other content creation tasks. When artifact is open, it is on the right side of the screen, while the conversation is on the left side. When creating or updating documents, changes are reflected in real-time on the artifacts and visible to the user.
|
||||
|
||||
When asked to write code, always use blocks. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language.
|
||||
When asked to write code, always use artifacts. When writing code, specify the language in the backticks, e.g. \`\`\`python\`code here\`\`\`. The default language is Python. Other languages are not yet supported, so let the user know if they request a different language.
|
||||
|
||||
DO NOT UPDATE DOCUMENTS IMMEDIATELY AFTER CREATING THEM. WAIT FOR USER FEEDBACK OR REQUEST TO UPDATE IT.
|
||||
|
||||
This is a guide for using blocks tools: \`createDocument\` and \`updateDocument\`, which render content on a blocks beside the conversation.
|
||||
This is a guide for using artifacts tools: \`createDocument\` and \`updateDocument\`, which render content on a artifacts beside the conversation.
|
||||
|
||||
**When to use \`createDocument\`:**
|
||||
- For substantial content (>10 lines) or code
|
||||
|
|
@ -42,7 +42,7 @@ export const systemPrompt = ({
|
|||
if (selectedChatModel === 'chat-model-reasoning') {
|
||||
return regularPrompt;
|
||||
} else {
|
||||
return `${regularPrompt}\n\n${blocksPrompt}`;
|
||||
return `${regularPrompt}\n\n${artifactsPrompt}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ You are a spreadsheet creation assistant. Create a spreadsheet in csv format bas
|
|||
|
||||
export const updateDocumentPrompt = (
|
||||
currentContent: string | null,
|
||||
type: BlockKind,
|
||||
type: ArtifactKind,
|
||||
) =>
|
||||
type === 'text'
|
||||
? `\
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { generateUUID } from '@/lib/utils';
|
|||
import { DataStreamWriter, tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { Session } from 'next-auth';
|
||||
import { blockKinds, documentHandlersByBlockKind } from '@/lib/blocks/server';
|
||||
import {
|
||||
artifactKinds,
|
||||
documentHandlersByArtifactKind,
|
||||
} from '@/lib/artifacts/server';
|
||||
|
||||
interface CreateDocumentProps {
|
||||
session: Session;
|
||||
|
|
@ -15,7 +18,7 @@ export const createDocument = ({ session, dataStream }: CreateDocumentProps) =>
|
|||
'Create a document for a writing or content creation activities. This tool will call other functions that will generate the contents of the document based on the title and kind.',
|
||||
parameters: z.object({
|
||||
title: z.string(),
|
||||
kind: z.enum(blockKinds),
|
||||
kind: z.enum(artifactKinds),
|
||||
}),
|
||||
execute: async ({ title, kind }) => {
|
||||
const id = generateUUID();
|
||||
|
|
@ -40,9 +43,9 @@ export const createDocument = ({ session, dataStream }: CreateDocumentProps) =>
|
|||
content: '',
|
||||
});
|
||||
|
||||
const documentHandler = documentHandlersByBlockKind.find(
|
||||
(documentHandlerByBlockKind) =>
|
||||
documentHandlerByBlockKind.kind === kind,
|
||||
const documentHandler = documentHandlersByArtifactKind.find(
|
||||
(documentHandlerByArtifactKind) =>
|
||||
documentHandlerByArtifactKind.kind === kind,
|
||||
);
|
||||
|
||||
if (!documentHandler) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const requestSuggestions = ({
|
|||
> = [];
|
||||
|
||||
const { elementStream } = streamObject({
|
||||
model: myProvider.languageModel('block-model'),
|
||||
model: myProvider.languageModel('artifact-model'),
|
||||
system:
|
||||
'You are a help writing assistant. Given a piece of writing, please offer suggestions to improve the piece of writing and describe the change. It is very important for the edits to contain full sentences instead of just words. Max 5 suggestions.',
|
||||
prompt: document.content,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { DataStreamWriter, tool } from 'ai';
|
|||
import { Session } from 'next-auth';
|
||||
import { z } from 'zod';
|
||||
import { getDocumentById, saveDocument } from '@/lib/db/queries';
|
||||
import { documentHandlersByBlockKind } from '@/lib/blocks/server';
|
||||
import { documentHandlersByArtifactKind } from '@/lib/artifacts/server';
|
||||
|
||||
interface UpdateDocumentProps {
|
||||
session: Session;
|
||||
|
|
@ -32,9 +32,9 @@ export const updateDocument = ({ session, dataStream }: UpdateDocumentProps) =>
|
|||
content: document.title,
|
||||
});
|
||||
|
||||
const documentHandler = documentHandlersByBlockKind.find(
|
||||
(documentHandlerByBlockKind) =>
|
||||
documentHandlerByBlockKind.kind === document.kind,
|
||||
const documentHandler = documentHandlersByArtifactKind.find(
|
||||
(documentHandlerByArtifactKind) =>
|
||||
documentHandlerByArtifactKind.kind === document.kind,
|
||||
);
|
||||
|
||||
if (!documentHandler) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { codeDocumentHandler } from '@/blocks/code/server';
|
||||
import { imageDocumentHandler } from '@/blocks/image/server';
|
||||
import { sheetDocumentHandler } from '@/blocks/sheet/server';
|
||||
import { textDocumentHandler } from '@/blocks/text/server';
|
||||
import { BlockKind } from '@/components/block';
|
||||
import { codeDocumentHandler } from '@/artifacts/code/server';
|
||||
import { imageDocumentHandler } from '@/artifacts/image/server';
|
||||
import { sheetDocumentHandler } from '@/artifacts/sheet/server';
|
||||
import { textDocumentHandler } from '@/artifacts/text/server';
|
||||
import { ArtifactKind } from '@/components/artifact';
|
||||
import { DataStreamWriter } from 'ai';
|
||||
import { Document } from '../db/schema';
|
||||
import { saveDocument } from '../db/queries';
|
||||
|
|
@ -11,7 +11,7 @@ import { Session } from 'next-auth';
|
|||
export interface SaveDocumentProps {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: BlockKind;
|
||||
kind: ArtifactKind;
|
||||
content: string;
|
||||
userId: string;
|
||||
}
|
||||
|
|
@ -30,13 +30,13 @@ export interface UpdateDocumentCallbackProps {
|
|||
session: Session;
|
||||
}
|
||||
|
||||
export interface DocumentHandler<T = BlockKind> {
|
||||
export interface DocumentHandler<T = ArtifactKind> {
|
||||
kind: T;
|
||||
onCreateDocument: (args: CreateDocumentCallbackProps) => Promise<void>;
|
||||
onUpdateDocument: (args: UpdateDocumentCallbackProps) => Promise<void>;
|
||||
}
|
||||
|
||||
export function createDocumentHandler<T extends BlockKind>(config: {
|
||||
export function createDocumentHandler<T extends ArtifactKind>(config: {
|
||||
kind: T;
|
||||
onCreateDocument: (params: CreateDocumentCallbackProps) => Promise<string>;
|
||||
onUpdateDocument: (params: UpdateDocumentCallbackProps) => Promise<string>;
|
||||
|
|
@ -87,13 +87,13 @@ export function createDocumentHandler<T extends BlockKind>(config: {
|
|||
}
|
||||
|
||||
/*
|
||||
* Use this array to define the document handlers for each block kind.
|
||||
* Use this array to define the document handlers for each artifact kind.
|
||||
*/
|
||||
export const documentHandlersByBlockKind: Array<DocumentHandler> = [
|
||||
export const documentHandlersByArtifactKind: Array<DocumentHandler> = [
|
||||
textDocumentHandler,
|
||||
codeDocumentHandler,
|
||||
imageDocumentHandler,
|
||||
sheetDocumentHandler,
|
||||
];
|
||||
|
||||
export const blockKinds = ['text', 'code', 'image', 'sheet'] as const;
|
||||
export const artifactKinds = ['text', 'code', 'image', 'sheet'] as const;
|
||||
|
|
@ -16,7 +16,7 @@ import {
|
|||
message,
|
||||
vote,
|
||||
} from './schema';
|
||||
import { BlockKind } from '@/components/block';
|
||||
import { ArtifactKind } from '@/components/artifact';
|
||||
|
||||
// Optionally, if not using email/pass login, you can
|
||||
// use the Drizzle adapter for Auth.js / NextAuth
|
||||
|
|
@ -176,7 +176,7 @@ export async function saveDocument({
|
|||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: BlockKind;
|
||||
kind: ArtifactKind;
|
||||
content: string;
|
||||
userId: string;
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {
|
|||
foreignKey,
|
||||
boolean,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { blockKinds } from '../blocks/server';
|
||||
|
||||
export const user = pgTable('User', {
|
||||
id: uuid('id').primaryKey().notNull().defaultRandom(),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { createRoot } from 'react-dom/client';
|
|||
|
||||
import { Suggestion as PreviewSuggestion } from '@/components/suggestion';
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import { BlockKind } from '@/components/block';
|
||||
import { ArtifactKind } from '@/components/artifact';
|
||||
|
||||
export interface UISuggestion extends Suggestion {
|
||||
selectionStart: number;
|
||||
|
|
@ -70,7 +70,7 @@ export function projectWithPositions(
|
|||
export function createSuggestionWidget(
|
||||
suggestion: UISuggestion,
|
||||
view: EditorView,
|
||||
blockKind: BlockKind = 'text',
|
||||
artifactKind: ArtifactKind = 'text',
|
||||
): { dom: HTMLElement; destroy: () => void } {
|
||||
const dom = document.createElement('span');
|
||||
const root = createRoot(dom);
|
||||
|
|
@ -117,7 +117,7 @@ export function createSuggestionWidget(
|
|||
<PreviewSuggestion
|
||||
suggestion={suggestion}
|
||||
onApply={onApply}
|
||||
blockKind={blockKind}
|
||||
artifactKind={artifactKind}
|
||||
/>,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue