Rename to blocks (#502)

This commit is contained in:
Jeremy 2024-11-07 02:40:29 +03:00 committed by GitHub
parent 22a09de6e6
commit 729634e1d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 108 additions and 111 deletions

View file

@ -21,8 +21,8 @@ export const models: Array<Model> = [
description: 'For complex, multi-step tasks', description: 'For complex, multi-step tasks',
}, },
{ {
id: 'gpt-4o-canvas', id: 'gpt-4o-blocks',
label: 'GPT 4o with Canvas', label: 'GPT 4o with Blocks',
apiIdentifier: 'gpt-4o', apiIdentifier: 'gpt-4o',
description: 'Collaborate with writing', description: 'Collaborate with writing',
}, },

View file

@ -1,7 +1,7 @@
export const canvasPrompt = ` export const blocksPrompt = `
Canvas is a special user interface mode that helps users with writing, editing, and other content creation tasks. When canvas 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 canvas and visible to the user. 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.
This is a guide for using canvas tools: \`createDocument\` and \`updateDocument\`, which render content on a canvas beside the conversation. This is a guide for using blocks tools: \`createDocument\` and \`updateDocument\`, which render content on a blocks beside the conversation.
**When to use \`createDocument\`:** **When to use \`createDocument\`:**
- For substantial content (>10 lines) - For substantial content (>10 lines)

View file

@ -9,7 +9,7 @@ import { z } from 'zod';
import { customModel } from '@/ai'; import { customModel } from '@/ai';
import { models } from '@/ai/models'; import { models } from '@/ai/models';
import { canvasPrompt, regularPrompt } from '@/ai/prompts'; import { blocksPrompt, regularPrompt } from '@/ai/prompts';
import { auth } from '@/app/(auth)/auth'; import { auth } from '@/app/(auth)/auth';
import { import {
deleteChatById, deleteChatById,
@ -37,7 +37,7 @@ type AllowedTools =
| 'requestSuggestions' | 'requestSuggestions'
| 'getWeather'; | 'getWeather';
const canvasTools: AllowedTools[] = [ const blocksTools: AllowedTools[] = [
'createDocument', 'createDocument',
'updateDocument', 'updateDocument',
'requestSuggestions', 'requestSuggestions',
@ -89,11 +89,11 @@ export async function POST(request: Request) {
const result = await streamText({ const result = await streamText({
model: customModel(model.apiIdentifier), model: customModel(model.apiIdentifier),
system: modelId === 'gpt-4o-canvas' ? canvasPrompt : regularPrompt, system: modelId === 'gpt-4o-blocks' ? blocksPrompt : regularPrompt,
messages: coreMessages, messages: coreMessages,
maxSteps: 5, maxSteps: 5,
experimental_activeTools: experimental_activeTools:
modelId === 'gpt-4o-canvas' ? canvasTools : weatherTools, modelId === 'gpt-4o-blocks' ? blocksTools : weatherTools,
tools: { tools: {
getWeather: { getWeather: {
description: 'Get the current weather at a location', description: 'Get the current weather at a location',

View file

@ -1,29 +1,29 @@
import { JSONValue } from 'ai'; import { JSONValue } from 'ai';
import { Dispatch, memo, SetStateAction } from 'react'; import { Dispatch, memo, SetStateAction } from 'react';
import { UICanvas } from './canvas'; import { UIBlock } from './block';
import { useCanvasStream } from './use-canvas-stream'; import { useBlockStream } from './use-block-stream';
interface CanvasStreamHandlerProps { interface BlockStreamHandlerProps {
setCanvas: Dispatch<SetStateAction<UICanvas>>; setBlock: Dispatch<SetStateAction<UIBlock>>;
streamingData: JSONValue[] | undefined; streamingData: JSONValue[] | undefined;
} }
export function PureCanvasStreamHandler({ export function PureBlockStreamHandler({
setCanvas, setBlock,
streamingData, streamingData,
}: CanvasStreamHandlerProps) { }: BlockStreamHandlerProps) {
useCanvasStream({ useBlockStream({
streamingData, streamingData,
setCanvas, setBlock,
}); });
return null; return null;
} }
function areEqual( function areEqual(
prevProps: CanvasStreamHandlerProps, prevProps: BlockStreamHandlerProps,
nextProps: CanvasStreamHandlerProps nextProps: BlockStreamHandlerProps
) { ) {
if (!prevProps.streamingData && !nextProps.streamingData) { if (!prevProps.streamingData && !nextProps.streamingData) {
return true; return true;
@ -40,4 +40,4 @@ function areEqual(
return true; return true;
} }
export const CanvasStreamHandler = memo(PureCanvasStreamHandler, areEqual); export const BlockStreamHandler = memo(PureBlockStreamHandler, areEqual);

View file

@ -31,7 +31,7 @@ import { useScrollToBottom } from './use-scroll-to-bottom';
import { VersionFooter } from './version-footer'; import { VersionFooter } from './version-footer';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
export interface UICanvas { export interface UIBlock {
title: string; title: string;
documentId: string; documentId: string;
content: string; content: string;
@ -45,7 +45,7 @@ export interface UICanvas {
}; };
} }
export function Canvas({ export function Block({
chatId, chatId,
input, input,
setInput, setInput,
@ -55,8 +55,8 @@ export function Canvas({
attachments, attachments,
setAttachments, setAttachments,
append, append,
canvas, block,
setCanvas, setBlock,
messages, messages,
setMessages, setMessages,
votes, votes,
@ -68,8 +68,8 @@ export function Canvas({
stop: () => void; stop: () => void;
attachments: Array<Attachment>; attachments: Array<Attachment>;
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>; setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
canvas: UICanvas; block: UIBlock;
setCanvas: Dispatch<SetStateAction<UICanvas>>; setBlock: Dispatch<SetStateAction<UIBlock>>;
messages: Array<Message>; messages: Array<Message>;
setMessages: Dispatch<SetStateAction<Array<Message>>>; setMessages: Dispatch<SetStateAction<Array<Message>>>;
votes: Array<Vote> | undefined; votes: Array<Vote> | undefined;
@ -92,15 +92,15 @@ export function Canvas({
isLoading: isDocumentsFetching, isLoading: isDocumentsFetching,
mutate: mutateDocuments, mutate: mutateDocuments,
} = useSWR<Array<Document>>( } = useSWR<Array<Document>>(
canvas && canvas.status !== 'streaming' block && block.status !== 'streaming'
? `/api/document?id=${canvas.documentId}` ? `/api/document?id=${block.documentId}`
: null, : null,
fetcher fetcher
); );
const { data: suggestions } = useSWR<Array<Suggestion>>( const { data: suggestions } = useSWR<Array<Suggestion>>(
documents && canvas && canvas.status !== 'streaming' documents && block && block.status !== 'streaming'
? `/api/suggestions?documentId=${canvas.documentId}` ? `/api/suggestions?documentId=${block.documentId}`
: null, : null,
fetcher, fetcher,
{ {
@ -119,27 +119,27 @@ export function Canvas({
if (mostRecentDocument) { if (mostRecentDocument) {
setDocument(mostRecentDocument); setDocument(mostRecentDocument);
setCurrentVersionIndex(documents.length - 1); setCurrentVersionIndex(documents.length - 1);
setCanvas((currentCanvas) => ({ setBlock((currentBlock) => ({
...currentCanvas, ...currentBlock,
content: mostRecentDocument.content ?? '', content: mostRecentDocument.content ?? '',
})); }));
} }
} }
}, [documents, setCanvas]); }, [documents, setBlock]);
useEffect(() => { useEffect(() => {
mutateDocuments(); mutateDocuments();
}, [canvas.status, mutateDocuments]); }, [block.status, mutateDocuments]);
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [isContentDirty, setIsContentDirty] = useState(false); const [isContentDirty, setIsContentDirty] = useState(false);
const handleContentChange = useCallback( const handleContentChange = useCallback(
(updatedContent: string) => { (updatedContent: string) => {
if (!canvas) return; if (!block) return;
mutate<Array<Document>>( mutate<Array<Document>>(
`/api/document?id=${canvas.documentId}`, `/api/document?id=${block.documentId}`,
async (currentDocuments) => { async (currentDocuments) => {
if (!currentDocuments) return undefined; if (!currentDocuments) return undefined;
@ -151,10 +151,10 @@ export function Canvas({
} }
if (currentDocument.content !== updatedContent) { if (currentDocument.content !== updatedContent) {
await fetch(`/api/document?id=${canvas.documentId}`, { await fetch(`/api/document?id=${block.documentId}`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
title: canvas.title, title: block.title,
content: updatedContent, content: updatedContent,
}), }),
}); });
@ -175,7 +175,7 @@ export function Canvas({
{ revalidate: false } { revalidate: false }
); );
}, },
[canvas, mutate] [block, mutate]
); );
const debouncedHandleContentChange = useDebounceCallback( const debouncedHandleContentChange = useDebounceCallback(
@ -295,8 +295,8 @@ export function Canvas({
chatId={chatId} chatId={chatId}
key={message.id} key={message.id}
message={message} message={message}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
isLoading={isLoading && index === messages.length - 1} isLoading={isLoading && index === messages.length - 1}
vote={ vote={
votes votes
@ -346,10 +346,10 @@ export function Canvas({
} }
: { : {
opacity: 0, opacity: 0,
x: canvas.boundingBox.left, x: block.boundingBox.left,
y: canvas.boundingBox.top, y: block.boundingBox.top,
height: canvas.boundingBox.height, height: block.boundingBox.height,
width: canvas.boundingBox.width, width: block.boundingBox.width,
borderRadius: 50, borderRadius: 50,
} }
} }
@ -401,8 +401,8 @@ export function Canvas({
variant="outline" variant="outline"
className="h-fit p-2 dark:hover:bg-zinc-700" className="h-fit p-2 dark:hover:bg-zinc-700"
onClick={() => { onClick={() => {
setCanvas((currentCanvas) => ({ setBlock((currentBlock) => ({
...currentCanvas, ...currentBlock,
isVisible: false, isVisible: false,
})); }));
}} }}
@ -412,7 +412,7 @@ export function Canvas({
<div className="flex flex-col"> <div className="flex flex-col">
<div className="font-medium"> <div className="font-medium">
{document?.title ?? canvas.title} {document?.title ?? block.title}
</div> </div>
{isContentDirty ? ( {isContentDirty ? (
@ -442,10 +442,10 @@ export function Canvas({
variant="outline" variant="outline"
className="p-2 h-fit dark:hover:bg-zinc-700" className="p-2 h-fit dark:hover:bg-zinc-700"
onClick={() => { onClick={() => {
copyToClipboard(canvas.content); copyToClipboard(block.content);
toast.success('Copied to clipboard!'); toast.success('Copied to clipboard!');
}} }}
disabled={canvas.status === 'streaming'} disabled={block.status === 'streaming'}
> >
<CopyIcon size={18} /> <CopyIcon size={18} />
</Button> </Button>
@ -461,7 +461,7 @@ export function Canvas({
handleVersionChange('prev'); handleVersionChange('prev');
}} }}
disabled={ disabled={
currentVersionIndex === 0 || canvas.status === 'streaming' currentVersionIndex === 0 || block.status === 'streaming'
} }
> >
<UndoIcon size={18} /> <UndoIcon size={18} />
@ -477,7 +477,7 @@ export function Canvas({
onClick={() => { onClick={() => {
handleVersionChange('next'); handleVersionChange('next');
}} }}
disabled={isCurrentVersion || canvas.status === 'streaming'} disabled={isCurrentVersion || block.status === 'streaming'}
> >
<RedoIcon size={18} /> <RedoIcon size={18} />
</Button> </Button>
@ -498,7 +498,7 @@ export function Canvas({
handleVersionChange('toggle'); handleVersionChange('toggle');
}} }}
disabled={ disabled={
canvas.status === 'streaming' || currentVersionIndex === 0 block.status === 'streaming' || currentVersionIndex === 0
} }
> >
<DeltaIcon size={18} /> <DeltaIcon size={18} />
@ -511,18 +511,18 @@ export function Canvas({
<div className="prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll px-4 py-8 md:p-20 !max-w-full pb-40 items-center"> <div className="prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll px-4 py-8 md:p-20 !max-w-full pb-40 items-center">
<div className="flex flex-row max-w-[600px] mx-auto"> <div className="flex flex-row max-w-[600px] mx-auto">
{isDocumentsFetching && !canvas.content ? ( {isDocumentsFetching && !block.content ? (
<DocumentSkeleton /> <DocumentSkeleton />
) : mode === 'edit' ? ( ) : mode === 'edit' ? (
<Editor <Editor
content={ content={
isCurrentVersion isCurrentVersion
? canvas.content ? block.content
: getDocumentContentById(currentVersionIndex) : getDocumentContentById(currentVersionIndex)
} }
isCurrentVersion={isCurrentVersion} isCurrentVersion={isCurrentVersion}
currentVersionIndex={currentVersionIndex} currentVersionIndex={currentVersionIndex}
status={canvas.status} status={block.status}
saveContent={saveContent} saveContent={saveContent}
suggestions={isCurrentVersion ? (suggestions ?? []) : []} suggestions={isCurrentVersion ? (suggestions ?? []) : []}
/> />
@ -555,7 +555,7 @@ export function Canvas({
<AnimatePresence> <AnimatePresence>
{!isCurrentVersion && ( {!isCurrentVersion && (
<VersionFooter <VersionFooter
canvas={canvas} block={block}
currentVersionIndex={currentVersionIndex} currentVersionIndex={currentVersionIndex}
documents={documents} documents={documents}
handleVersionChange={handleVersionChange} handleVersionChange={handleVersionChange}

View file

@ -13,8 +13,8 @@ import { useScrollToBottom } from '@/components/custom/use-scroll-to-bottom';
import { Vote } from '@/db/schema'; import { Vote } from '@/db/schema';
import { fetcher } from '@/lib/utils'; import { fetcher } from '@/lib/utils';
import { Canvas, UICanvas } from './canvas'; import { Block, UIBlock } from './block';
import { CanvasStreamHandler } from './canvas-stream-handler'; import { BlockStreamHandler } from './block-stream-handler';
import { MultimodalInput } from './multimodal-input'; import { MultimodalInput } from './multimodal-input';
import { Overview } from './overview'; import { Overview } from './overview';
@ -50,7 +50,7 @@ export function Chat({
const { width: windowWidth = 1920, height: windowHeight = 1080 } = const { width: windowWidth = 1920, height: windowHeight = 1080 } =
useWindowSize(); useWindowSize();
const [canvas, setCanvas] = useState<UICanvas>({ const [block, setBlock] = useState<UIBlock>({
documentId: 'init', documentId: 'init',
content: '', content: '',
title: '', title: '',
@ -89,8 +89,8 @@ export function Chat({
key={message.id} key={message.id}
chatId={id} chatId={id}
message={message} message={message}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
isLoading={isLoading && messages.length - 1 === index} isLoading={isLoading && messages.length - 1 === index}
vote={ vote={
votes votes
@ -129,8 +129,8 @@ export function Chat({
</div> </div>
<AnimatePresence> <AnimatePresence>
{canvas && canvas.isVisible && ( {block && block.isVisible && (
<Canvas <Block
chatId={id} chatId={id}
input={input} input={input}
setInput={setInput} setInput={setInput}
@ -140,8 +140,8 @@ export function Chat({
attachments={attachments} attachments={attachments}
setAttachments={setAttachments} setAttachments={setAttachments}
append={append} append={append}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
messages={messages} messages={messages}
setMessages={setMessages} setMessages={setMessages}
votes={votes} votes={votes}
@ -149,10 +149,7 @@ export function Chat({
)} )}
</AnimatePresence> </AnimatePresence>
<CanvasStreamHandler <BlockStreamHandler streamingData={streamingData} setBlock={setBlock} />
streamingData={streamingData}
setCanvas={setCanvas}
/>
</> </>
); );
} }

View file

@ -1,6 +1,6 @@
import { SetStateAction } from 'react'; import { SetStateAction } from 'react';
import { UICanvas } from './canvas'; import { UIBlock } from './block';
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons'; import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
const getActionText = (type: 'create' | 'update' | 'request-suggestions') => { const getActionText = (type: 'create' | 'update' | 'request-suggestions') => {
@ -19,15 +19,15 @@ const getActionText = (type: 'create' | 'update' | 'request-suggestions') => {
interface DocumentToolResultProps { interface DocumentToolResultProps {
type: 'create' | 'update' | 'request-suggestions'; type: 'create' | 'update' | 'request-suggestions';
result: any; result: any;
canvas: UICanvas; block: UIBlock;
setCanvas: (value: SetStateAction<UICanvas>) => void; setBlock: (value: SetStateAction<UIBlock>) => void;
} }
export function DocumentToolResult({ export function DocumentToolResult({
type, type,
result, result,
canvas, block,
setCanvas, setBlock,
}: DocumentToolResultProps) { }: DocumentToolResultProps) {
return ( return (
<div <div
@ -42,7 +42,7 @@ export function DocumentToolResult({
height: rect.height, height: rect.height,
}; };
setCanvas({ setBlock({
documentId: result.id, documentId: result.id,
content: '', content: '',
title: result.title, title: result.title,

View file

@ -7,7 +7,7 @@ import { Dispatch, SetStateAction } from 'react';
import { Vote } from '@/db/schema'; import { Vote } from '@/db/schema';
import { UICanvas } from './canvas'; import { UIBlock } from './block';
import { DocumentToolCall, DocumentToolResult } from './document'; import { DocumentToolCall, DocumentToolResult } from './document';
import { SparklesIcon } from './icons'; import { SparklesIcon } from './icons';
import { Markdown } from './markdown'; import { Markdown } from './markdown';
@ -18,15 +18,15 @@ import { Weather } from './weather';
export const PreviewMessage = ({ export const PreviewMessage = ({
chatId, chatId,
message, message,
canvas, block,
setCanvas, setBlock,
vote, vote,
isLoading, isLoading,
}: { }: {
chatId: string; chatId: string;
message: Message; message: Message;
canvas: UICanvas; block: UIBlock;
setCanvas: Dispatch<SetStateAction<UICanvas>>; setBlock: Dispatch<SetStateAction<UIBlock>>;
vote: Vote | undefined; vote: Vote | undefined;
isLoading: boolean; isLoading: boolean;
}) => { }) => {
@ -71,22 +71,22 @@ export const PreviewMessage = ({
<DocumentToolResult <DocumentToolResult
type="create" type="create"
result={result} result={result}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
/> />
) : toolName === 'updateDocument' ? ( ) : toolName === 'updateDocument' ? (
<DocumentToolResult <DocumentToolResult
type="update" type="update"
result={result} result={result}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
/> />
) : toolName === 'requestSuggestions' ? ( ) : toolName === 'requestSuggestions' ? (
<DocumentToolResult <DocumentToolResult
type="request-suggestions" type="request-suggestions"
result={result} result={result}
canvas={canvas} block={block}
setCanvas={setCanvas} setBlock={setBlock}
/> />
) : ( ) : (
<pre>{JSON.stringify(result, null, 2)}</pre> <pre>{JSON.stringify(result, null, 2)}</pre>

View file

@ -4,19 +4,19 @@ import { useSWRConfig } from 'swr';
import { Suggestion } from '@/db/schema'; import { Suggestion } from '@/db/schema';
import { UICanvas } from './canvas'; import { UIBlock } from './block';
type StreamingDelta = { type StreamingDelta = {
type: 'text-delta' | 'title' | 'id' | 'suggestion' | 'clear' | 'finish'; type: 'text-delta' | 'title' | 'id' | 'suggestion' | 'clear' | 'finish';
content: string | Suggestion; content: string | Suggestion;
}; };
export function useCanvasStream({ export function useBlockStream({
streamingData, streamingData,
setCanvas, setBlock,
}: { }: {
streamingData: JSONValue[] | undefined; streamingData: JSONValue[] | undefined;
setCanvas: Dispatch<SetStateAction<UICanvas>>; setBlock: Dispatch<SetStateAction<UIBlock>>;
}) { }) {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const [optimisticSuggestions, setOptimisticSuggestions] = useState< const [optimisticSuggestions, setOptimisticSuggestions] = useState<
@ -37,30 +37,30 @@ export function useCanvasStream({
const delta = mostRecentDelta as StreamingDelta; const delta = mostRecentDelta as StreamingDelta;
setCanvas((draftCanvas) => { setBlock((draftBlock) => {
switch (delta.type) { switch (delta.type) {
case 'id': case 'id':
return { return {
...draftCanvas, ...draftBlock,
documentId: delta.content as string, documentId: delta.content as string,
}; };
case 'title': case 'title':
return { return {
...draftCanvas, ...draftBlock,
title: delta.content as string, title: delta.content as string,
}; };
case 'text-delta': case 'text-delta':
return { return {
...draftCanvas, ...draftBlock,
content: draftCanvas.content + (delta.content as string), content: draftBlock.content + (delta.content as string),
isVisible: isVisible:
draftCanvas.status === 'streaming' && draftBlock.status === 'streaming' &&
draftCanvas.content.length > 200 && draftBlock.content.length > 200 &&
draftCanvas.content.length < 250 draftBlock.content.length < 250
? true ? true
: draftCanvas.isVisible, : draftBlock.isVisible,
status: 'streaming', status: 'streaming',
}; };
@ -72,24 +72,24 @@ export function useCanvasStream({
]); ]);
}, 0); }, 0);
return draftCanvas; return draftBlock;
case 'clear': case 'clear':
return { return {
...draftCanvas, ...draftBlock,
content: '', content: '',
status: 'streaming', status: 'streaming',
}; };
case 'finish': case 'finish':
return { return {
...draftCanvas, ...draftBlock,
status: 'idle', status: 'idle',
}; };
default: default:
return draftCanvas; return draftBlock;
} }
}); });
}, [streamingData, setCanvas]); }, [streamingData, setBlock]);
} }

View file

@ -9,19 +9,19 @@ import { useWindowSize } from 'usehooks-ts';
import { Document } from '@/db/schema'; import { Document } from '@/db/schema';
import { getDocumentTimestampByIndex } from '@/lib/utils'; import { getDocumentTimestampByIndex } from '@/lib/utils';
import { UICanvas } from './canvas'; import { UIBlock } from './block';
import { LoaderIcon } from './icons'; import { LoaderIcon } from './icons';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
interface VersionFooterProps { interface VersionFooterProps {
canvas: UICanvas; block: UIBlock;
handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void;
documents: Array<Document> | undefined; documents: Array<Document> | undefined;
currentVersionIndex: number; currentVersionIndex: number;
} }
export const VersionFooter = ({ export const VersionFooter = ({
canvas, block,
handleVersionChange, handleVersionChange,
documents, documents,
currentVersionIndex, currentVersionIndex,
@ -56,8 +56,8 @@ export const VersionFooter = ({
setIsMutating(true); setIsMutating(true);
mutate( mutate(
`/api/document?id=${canvas.documentId}`, `/api/document?id=${block.documentId}`,
await fetch(`/api/document?id=${canvas.documentId}`, { await fetch(`/api/document?id=${block.documentId}`, {
method: 'PATCH', method: 'PATCH',
body: JSON.stringify({ body: JSON.stringify({
timestamp: getDocumentTimestampByIndex( timestamp: getDocumentTimestampByIndex(