feat: add inline block document previews (#637)

This commit is contained in:
Jeremy 2024-12-19 17:05:04 +05:30 committed by GitHub
parent b659dcce68
commit bb03c516b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1122 additions and 805 deletions

View file

@ -3,6 +3,7 @@ import { cookies } from 'next/headers';
import { Chat } from '@/components/chat'; import { Chat } from '@/components/chat';
import { DEFAULT_MODEL_NAME, models } from '@/lib/ai/models'; import { DEFAULT_MODEL_NAME, models } from '@/lib/ai/models';
import { generateUUID } from '@/lib/utils'; import { generateUUID } from '@/lib/utils';
import { DataStreamHandler } from '@/components/data-stream-handler';
export default async function Page() { export default async function Page() {
const id = generateUUID(); const id = generateUUID();
@ -15,6 +16,7 @@ export default async function Page() {
DEFAULT_MODEL_NAME; DEFAULT_MODEL_NAME;
return ( return (
<>
<Chat <Chat
key={id} key={id}
id={id} id={id}
@ -23,5 +25,7 @@ export default async function Page() {
selectedVisibilityType="private" selectedVisibilityType="private"
isReadonly={false} isReadonly={false}
/> />
<DataStreamHandler id={id} />
</>
); );
} }

View file

@ -45,7 +45,10 @@
"noSvgWithoutTitle": "off", // We do not intend to adhere to this rule "noSvgWithoutTitle": "off", // We do not intend to adhere to this rule
"useMediaCaption": "off", // We would need a cultural change to turn this on "useMediaCaption": "off", // We would need a cultural change to turn this on
"noAutofocus": "off", // We're highly intentional about when we use autofocus "noAutofocus": "off", // We're highly intentional about when we use autofocus
"noBlankTarget": "off" // Covered by Conformance "noBlankTarget": "off", // Covered by Conformance
"useFocusableInteractive": "off", // Disable focusable interactive element requirement
"useAriaPropsForRole": "off", // Disable required ARIA attributes check
"useKeyWithClickEvents": "off" // Disable keyboard event requirement with click events
}, },
"complexity": { "complexity": {
"noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually "noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually

View file

@ -1,23 +1,24 @@
import { memo, SetStateAction } from 'react'; import { memo } from 'react';
import { CrossIcon } from './icons'; import { CrossIcon } from './icons';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { UIBlock } from './block'; import { initialBlockData, useBlock } from '@/hooks/use-block';
import equal from 'fast-deep-equal';
interface BlockCloseButtonProps { function PureBlockCloseButton() {
setBlock: (value: SetStateAction<UIBlock>) => void; const { setBlock } = useBlock();
}
function PureBlockCloseButton({ setBlock }: BlockCloseButtonProps) {
return ( return (
<Button <Button
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={() => {
setBlock((currentBlock) => ({ setBlock((currentBlock) =>
currentBlock.status === 'streaming'
? {
...currentBlock, ...currentBlock,
isVisible: false, isVisible: false,
})); }
: { ...initialBlockData, status: 'idle' },
);
}} }}
> >
<CrossIcon size={18} /> <CrossIcon size={18} />

View file

@ -1,14 +1,13 @@
import { Dispatch, memo, SetStateAction } from 'react';
import { UIBlock } from './block';
import { PreviewMessage } from './message'; import { PreviewMessage } from './message';
import { useScrollToBottom } from './use-scroll-to-bottom'; import { useScrollToBottom } from './use-scroll-to-bottom';
import { Vote } from '@/lib/db/schema'; import { Vote } from '@/lib/db/schema';
import { ChatRequestOptions, Message } from 'ai'; import { ChatRequestOptions, Message } from 'ai';
import { memo } from 'react';
import equal from 'fast-deep-equal';
import { UIBlock } from './block';
interface BlockMessagesProps { interface BlockMessagesProps {
chatId: string; chatId: string;
block: UIBlock;
setBlock: Dispatch<SetStateAction<UIBlock>>;
isLoading: boolean; isLoading: boolean;
votes: Array<Vote> | undefined; votes: Array<Vote> | undefined;
messages: Array<Message>; messages: Array<Message>;
@ -19,12 +18,11 @@ interface BlockMessagesProps {
chatRequestOptions?: ChatRequestOptions, chatRequestOptions?: ChatRequestOptions,
) => Promise<string | null | undefined>; ) => Promise<string | null | undefined>;
isReadonly: boolean; isReadonly: boolean;
blockStatus: UIBlock['status'];
} }
function PureBlockMessages({ function PureBlockMessages({
chatId, chatId,
block,
setBlock,
isLoading, isLoading,
votes, votes,
messages, messages,
@ -45,8 +43,6 @@ function PureBlockMessages({
chatId={chatId} chatId={chatId}
key={message.id} key={message.id}
message={message} message={message}
block={block}
setBlock={setBlock}
isLoading={isLoading && index === messages.length - 1} isLoading={isLoading && index === messages.length - 1}
vote={ vote={
votes votes
@ -72,13 +68,17 @@ function areEqual(
nextProps: BlockMessagesProps, nextProps: BlockMessagesProps,
) { ) {
if ( if (
prevProps.block.status === 'streaming' && prevProps.blockStatus === 'streaming' &&
nextProps.block.status === 'streaming' nextProps.blockStatus === 'streaming'
) { )
return true;
if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.isLoading && nextProps.isLoading) return false;
if (prevProps.messages.length !== nextProps.messages.length) return false;
if (!equal(prevProps.votes, nextProps.votes)) return false;
return true; return true;
} }
return false;
}
export const BlockMessages = memo(PureBlockMessages, areEqual); export const BlockMessages = memo(PureBlockMessages, areEqual);

View file

@ -1,43 +0,0 @@
import type { JSONValue } from 'ai';
import { type Dispatch, memo, type SetStateAction } from 'react';
import type { UIBlock } from './block';
import { useBlockStream } from './use-block-stream';
interface BlockStreamHandlerProps {
setBlock: Dispatch<SetStateAction<UIBlock>>;
streamingData: JSONValue[] | undefined;
}
function PureBlockStreamHandler({
setBlock,
streamingData,
}: BlockStreamHandlerProps) {
useBlockStream({
streamingData,
setBlock,
});
return null;
}
function areEqual(
prevProps: BlockStreamHandlerProps,
nextProps: BlockStreamHandlerProps,
) {
if (!prevProps.streamingData && !nextProps.streamingData) {
return true;
}
if (!prevProps.streamingData || !nextProps.streamingData) {
return false;
}
if (prevProps.streamingData.length !== nextProps.streamingData.length) {
return false;
}
return true;
}
export const BlockStreamHandler = memo(PureBlockStreamHandler, areEqual);

View file

@ -31,6 +31,9 @@ import { BlockCloseButton } from './block-close-button';
import { BlockMessages } from './block-messages'; import { BlockMessages } from './block-messages';
import { CodeEditor } from './code-editor'; import { CodeEditor } from './code-editor';
import { Console } from './console'; import { Console } from './console';
import { useSidebar } from './ui/sidebar';
import { useBlock } from '@/hooks/use-block';
import equal from 'fast-deep-equal';
export type BlockKind = 'text' | 'code'; export type BlockKind = 'text' | 'code';
@ -65,8 +68,6 @@ function PureBlock({
attachments, attachments,
setAttachments, setAttachments,
append, append,
block,
setBlock,
messages, messages,
setMessages, setMessages,
reload, reload,
@ -80,8 +81,6 @@ function PureBlock({
stop: () => void; stop: () => void;
attachments: Array<Attachment>; attachments: Array<Attachment>;
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>; setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
block: UIBlock;
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;
@ -100,12 +99,14 @@ function PureBlock({
) => Promise<string | null | undefined>; ) => Promise<string | null | undefined>;
isReadonly: boolean; isReadonly: boolean;
}) { }) {
const { block, setBlock } = useBlock();
const { const {
data: documents, data: documents,
isLoading: isDocumentsFetching, isLoading: isDocumentsFetching,
mutate: mutateDocuments, mutate: mutateDocuments,
} = useSWR<Array<Document>>( } = useSWR<Array<Document>>(
block && block.status !== 'streaming' block.documentId !== 'init' && block.status !== 'streaming'
? `/api/document?id=${block.documentId}` ? `/api/document?id=${block.documentId}`
: null, : null,
fetcher, fetcher,
@ -128,6 +129,8 @@ function PureBlock({
[], [],
); );
const { open: isSidebarOpen } = useSidebar();
useEffect(() => { useEffect(() => {
if (documents && documents.length > 0) { if (documents && documents.length > 0) {
const mostRecentDocument = documents.at(-1); const mostRecentDocument = documents.at(-1);
@ -260,12 +263,29 @@ function PureBlock({
const isMobile = windowWidth ? windowWidth < 768 : false; const isMobile = windowWidth ? windowWidth < 768 : false;
return ( return (
<AnimatePresence>
{block.isVisible && (
<motion.div <motion.div
className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-muted" className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-transparent"
initial={{ opacity: 1 }} initial={{ opacity: 1 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { delay: 0.4 } }} exit={{ opacity: 0, transition: { delay: 0.4 } }}
> >
{!isMobile && (
<motion.div
className="fixed bg-background h-dvh"
initial={{
width: isSidebarOpen ? windowWidth - 256 : windowWidth,
right: 0,
}}
animate={{ width: windowWidth, right: 0 }}
exit={{
width: isSidebarOpen ? windowWidth - 256 : windowWidth,
right: 0,
}}
/>
)}
{!isMobile && ( {!isMobile && (
<motion.div <motion.div
className="relative w-[400px] bg-muted dark:bg-background h-dvh shrink-0" className="relative w-[400px] bg-muted dark:bg-background h-dvh shrink-0"
@ -284,8 +304,8 @@ function PureBlock({
exit={{ exit={{
opacity: 0, opacity: 0,
x: 0, x: 0,
scale: 0.95, scale: 1,
transition: { delay: 0 }, transition: { duration: 0 },
}} }}
> >
<AnimatePresence> <AnimatePresence>
@ -302,14 +322,13 @@ function PureBlock({
<div className="flex flex-col h-full justify-between items-center gap-4"> <div className="flex flex-col h-full justify-between items-center gap-4">
<BlockMessages <BlockMessages
chatId={chatId} chatId={chatId}
block={block}
isLoading={isLoading} isLoading={isLoading}
setBlock={setBlock}
votes={votes} votes={votes}
messages={messages} messages={messages}
setMessages={setMessages} setMessages={setMessages}
reload={reload} reload={reload}
isReadonly={isReadonly} isReadonly={isReadonly}
blockStatus={block.status}
/> />
<form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4"> <form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
@ -333,19 +352,19 @@ function PureBlock({
)} )}
<motion.div <motion.div
className="fixed dark:bg-muted bg-background h-dvh flex flex-col overflow-y-scroll" className="fixed dark:bg-muted bg-background h-dvh flex flex-col overflow-y-scroll border-l dark:border-zinc-700 border-zinc-200"
initial={ initial={
isMobile isMobile
? { ? {
opacity: 0, opacity: 1,
x: 0, x: block.boundingBox.left,
y: 0, y: block.boundingBox.top,
width: windowWidth, height: block.boundingBox.height,
height: windowHeight, width: block.boundingBox.width,
borderRadius: 50, borderRadius: 50,
} }
: { : {
opacity: 0, opacity: 1,
x: block.boundingBox.left, x: block.boundingBox.left,
y: block.boundingBox.top, y: block.boundingBox.top,
height: block.boundingBox.height, height: block.boundingBox.height,
@ -359,14 +378,15 @@ function PureBlock({
opacity: 1, opacity: 1,
x: 0, x: 0,
y: 0, y: 0,
width: windowWidth, height: windowHeight,
height: '100dvh', width: windowWidth ? windowWidth : 'calc(100dvw)',
borderRadius: 0, borderRadius: 0,
transition: { transition: {
delay: 0, delay: 0,
type: 'spring', type: 'spring',
stiffness: 200, stiffness: 200,
damping: 30, damping: 30,
duration: 5000,
}, },
} }
: { : {
@ -374,13 +394,16 @@ function PureBlock({
x: 400, x: 400,
y: 0, y: 0,
height: windowHeight, height: windowHeight,
width: windowWidth ? windowWidth - 400 : 'calc(100dvw-400px)', width: windowWidth
? windowWidth - 400
: 'calc(100dvw-400px)',
borderRadius: 0, borderRadius: 0,
transition: { transition: {
delay: 0, delay: 0,
type: 'spring', type: 'spring',
stiffness: 200, stiffness: 200,
damping: 30, damping: 30,
duration: 5000,
}, },
} }
} }
@ -397,7 +420,7 @@ function PureBlock({
> >
<div className="p-2 flex flex-row justify-between items-start"> <div className="p-2 flex flex-row justify-between items-start">
<div className="flex flex-row gap-4 items-start"> <div className="flex flex-row gap-4 items-start">
<BlockCloseButton setBlock={setBlock} /> <BlockCloseButton />
<div className="flex flex-col"> <div className="flex flex-col">
<div className="font-medium"> <div className="font-medium">
@ -436,7 +459,7 @@ function PureBlock({
<div <div
className={cn( className={cn(
'prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll !max-w-full pb-40 items-center', 'dark:bg-muted bg-background h-full overflow-y-scroll !max-w-full pb-40 items-center',
{ {
'py-2 px-2': block.kind === 'code', 'py-2 px-2': block.kind === 'code',
'py-8 md:p-20 px-4': block.kind === 'text', 'py-8 md:p-20 px-4': block.kind === 'text',
@ -480,7 +503,9 @@ function PureBlock({
/> />
) : ( ) : (
<DiffView <DiffView
oldContent={getDocumentContentById(currentVersionIndex - 1)} oldContent={getDocumentContentById(
currentVersionIndex - 1,
)}
newContent={getDocumentContentById(currentVersionIndex)} newContent={getDocumentContentById(currentVersionIndex)}
/> />
) )
@ -509,7 +534,6 @@ function PureBlock({
<AnimatePresence> <AnimatePresence>
{!isCurrentVersion && ( {!isCurrentVersion && (
<VersionFooter <VersionFooter
block={block}
currentVersionIndex={currentVersionIndex} currentVersionIndex={currentVersionIndex}
documents={documents} documents={documents}
handleVersionChange={handleVersionChange} handleVersionChange={handleVersionChange}
@ -525,9 +549,16 @@ function PureBlock({
</AnimatePresence> </AnimatePresence>
</motion.div> </motion.div>
</motion.div> </motion.div>
)}
</AnimatePresence>
); );
} }
export const Block = memo(PureBlock, (prevProps, nextProps) => { export const Block = memo(PureBlock, (prevProps, nextProps) => {
return false; if (prevProps.isLoading !== nextProps.isLoading) return false;
if (!equal(prevProps.votes, nextProps.votes)) return false;
if (prevProps.input !== nextProps.input) return false;
if (!equal(prevProps.messages, nextProps.messages.length)) return false;
return true;
}); });

View file

@ -2,20 +2,18 @@
import type { Attachment, Message } from 'ai'; import type { Attachment, Message } from 'ai';
import { useChat } from 'ai/react'; import { useChat } from 'ai/react';
import { AnimatePresence } from 'framer-motion';
import { useState } from 'react'; import { useState } from 'react';
import useSWR, { useSWRConfig } from 'swr'; import useSWR, { useSWRConfig } from 'swr';
import { useWindowSize } from 'usehooks-ts';
import { ChatHeader } from '@/components/chat-header'; import { ChatHeader } from '@/components/chat-header';
import type { Vote } from '@/lib/db/schema'; import type { Vote } from '@/lib/db/schema';
import { fetcher } from '@/lib/utils'; import { fetcher } from '@/lib/utils';
import { Block, type UIBlock } from './block'; import { Block } from './block';
import { BlockStreamHandler } from './block-stream-handler';
import { MultimodalInput } from './multimodal-input'; import { MultimodalInput } from './multimodal-input';
import { Messages } from './messages'; import { Messages } from './messages';
import { VisibilityType } from './visibility-selector'; import { VisibilityType } from './visibility-selector';
import { useBlockSelector } from '@/hooks/use-block';
export function Chat({ export function Chat({
id, id,
@ -42,40 +40,23 @@ export function Chat({
isLoading, isLoading,
stop, stop,
reload, reload,
data: streamingData,
} = useChat({ } = useChat({
id, id,
body: { id, modelId: selectedModelId }, body: { id, modelId: selectedModelId },
initialMessages, initialMessages,
experimental_throttle: 100,
onFinish: () => { onFinish: () => {
mutate('/api/history'); mutate('/api/history');
}, },
}); });
const { width: windowWidth = 1920, height: windowHeight = 1080 } =
useWindowSize();
const [block, setBlock] = useState<UIBlock>({
documentId: 'init',
content: '',
kind: 'text',
title: '',
status: 'idle',
isVisible: false,
boundingBox: {
top: windowHeight / 4,
left: windowWidth / 4,
width: 250,
height: 50,
},
});
const { data: votes } = useSWR<Array<Vote>>( const { data: votes } = useSWR<Array<Vote>>(
`/api/vote?chatId=${id}`, `/api/vote?chatId=${id}`,
fetcher, fetcher,
); );
const [attachments, setAttachments] = useState<Array<Attachment>>([]); const [attachments, setAttachments] = useState<Array<Attachment>>([]);
const isBlockVisible = useBlockSelector((state) => state.isVisible);
return ( return (
<> <>
@ -89,14 +70,13 @@ export function Chat({
<Messages <Messages
chatId={id} chatId={id}
block={block}
setBlock={setBlock}
isLoading={isLoading} isLoading={isLoading}
votes={votes} votes={votes}
messages={messages} messages={messages}
setMessages={setMessages} setMessages={setMessages}
reload={reload} reload={reload}
isReadonly={isReadonly} isReadonly={isReadonly}
isBlockVisible={isBlockVisible}
/> />
<form className="flex mx-auto px-4 bg-background pb-4 md:pb-6 gap-2 w-full md:max-w-3xl"> <form className="flex mx-auto px-4 bg-background pb-4 md:pb-6 gap-2 w-full md:max-w-3xl">
@ -118,8 +98,6 @@ export function Chat({
</form> </form>
</div> </div>
<AnimatePresence>
{block?.isVisible && (
<Block <Block
chatId={id} chatId={id}
input={input} input={input}
@ -130,18 +108,12 @@ export function Chat({
attachments={attachments} attachments={attachments}
setAttachments={setAttachments} setAttachments={setAttachments}
append={append} append={append}
block={block}
setBlock={setBlock}
messages={messages} messages={messages}
setMessages={setMessages} setMessages={setMessages}
reload={reload} reload={reload}
votes={votes} votes={votes}
isReadonly={isReadonly} isReadonly={isReadonly}
/> />
)}
</AnimatePresence>
<BlockStreamHandler streamingData={streamingData} setBlock={setBlock} />
</> </>
); );
} }

View file

@ -40,6 +40,8 @@ function PureCodeEditor({ content, saveContent, status }: EditorProps) {
editorRef.current = null; editorRef.current = null;
} }
}; };
// NOTE: we only want to run this effect once
// eslint-disable-next-line
}, []); }, []);
useEffect(() => { useEffect(() => {

View file

@ -63,6 +63,8 @@ export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
className="h-2 w-full fixed cursor-ns-resize z-50" className="h-2 w-full fixed cursor-ns-resize z-50"
onMouseDown={startResizing} onMouseDown={startResizing}
style={{ bottom: height - 4 }} style={{ bottom: height - 4 }}
role="slider"
aria-valuenow={minHeight}
/> />
<div <div

View file

@ -0,0 +1,117 @@
'use client';
import { useChat } from 'ai/react';
import { useEffect, useRef } from 'react';
import { BlockKind } from './block';
import { Suggestion } from '@/lib/db/schema';
import { initialBlockData, useBlock } from '@/hooks/use-block';
import { useUserMessageId } from '@/hooks/use-user-message-id';
import { cx } from 'class-variance-authority';
type DataStreamDelta = {
type:
| 'text-delta'
| 'code-delta'
| 'title'
| 'id'
| 'suggestion'
| 'clear'
| 'finish'
| 'user-message-id'
| 'kind';
content: string | Suggestion;
};
export function DataStreamHandler({ id }: { id: string }) {
const { data: dataStream } = useChat({ id });
const { setUserMessageIdFromServer } = useUserMessageId();
const { setBlock } = useBlock();
const lastProcessedIndex = useRef(-1);
useEffect(() => {
if (!dataStream?.length) return;
const newDeltas = dataStream.slice(lastProcessedIndex.current + 1);
lastProcessedIndex.current = dataStream.length - 1;
(newDeltas as DataStreamDelta[]).forEach((delta: DataStreamDelta) => {
if (delta.type === 'user-message-id') {
setUserMessageIdFromServer(delta.content as string);
return;
}
setBlock((draftBlock) => {
if (!draftBlock) {
return { ...initialBlockData, status: 'streaming' };
}
switch (delta.type) {
case 'id':
return {
...draftBlock,
documentId: delta.content as string,
status: 'streaming',
};
case 'title':
return {
...draftBlock,
title: delta.content as string,
status: 'streaming',
};
case 'kind':
return {
...draftBlock,
kind: delta.content as BlockKind,
status: 'streaming',
};
case 'text-delta':
return {
...draftBlock,
content: draftBlock.content + (delta.content as string),
isVisible:
draftBlock.status === 'streaming' &&
draftBlock.content.length > 400 &&
draftBlock.content.length < 450
? true
: draftBlock.isVisible,
status: 'streaming',
};
case 'code-delta':
return {
...draftBlock,
content: delta.content as string,
isVisible:
draftBlock.status === 'streaming' &&
draftBlock.content.length > 300 &&
draftBlock.content.length < 310
? true
: draftBlock.isVisible,
status: 'streaming',
};
case 'clear':
return {
...draftBlock,
content: '',
status: 'streaming',
};
case 'finish':
return {
...draftBlock,
status: 'idle',
};
default:
return draftBlock;
}
});
});
}, [dataStream, setBlock, setUserMessageIdFromServer]);
return null;
}

View file

@ -0,0 +1,245 @@
'use client';
import {
memo,
MouseEvent,
useCallback,
useEffect,
useMemo,
useRef,
} from 'react';
import { UIBlock } from './block';
import { FileIcon, FullscreenIcon, 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 { DocumentToolCall, DocumentToolResult } from './document';
import { CodeEditor } from './code-editor';
import { useBlock } from '@/hooks/use-block';
import equal from 'fast-deep-equal';
interface DocumentPreviewProps {
isReadonly: boolean;
result?: any;
args?: any;
}
export function DocumentPreview({
isReadonly,
result,
args,
}: DocumentPreviewProps) {
const { block, setBlock } = useBlock();
const { data: documents, isLoading: isDocumentsFetching } = useSWR<
Array<Document>
>(result ? `/api/document?id=${result.id}` : null, fetcher);
const previewDocument = useMemo(() => documents?.[0], [documents]);
const hitboxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const boundingBox = hitboxRef.current?.getBoundingClientRect();
if (block.documentId && boundingBox) {
setBlock((block) => ({
...block,
boundingBox: {
left: boundingBox.x,
top: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
},
}));
}
}, [block.documentId, setBlock]);
if (block.isVisible) {
if (result) {
return (
<DocumentToolResult
type="create"
result={{ id: result.id, title: result.title, kind: result.kind }}
isReadonly={isReadonly}
/>
);
}
if (args) {
return (
<DocumentToolCall
type="create"
args={{ title: args.title }}
isReadonly={isReadonly}
/>
);
}
}
if (isDocumentsFetching) {
return <LoadingSkeleton />;
}
const document: Document | null = previewDocument
? previewDocument
: block.status === 'streaming'
? {
title: block.title,
kind: block.kind,
content: block.content,
id: block.documentId,
createdAt: new Date(),
userId: 'noop',
}
: null;
if (!document) return <LoadingSkeleton />;
return (
<div className="relative w-full cursor-pointer">
<HitboxLayer hitboxRef={hitboxRef} result={result} setBlock={setBlock} />
<DocumentHeader
title={document.title}
isStreaming={block.status === 'streaming'}
/>
<DocumentContent document={document} />
</div>
);
}
const LoadingSkeleton = () => (
<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">
<div className="text-muted-foreground">
<div className="animate-pulse rounded-md size-4 bg-muted-foreground/20" />
</div>
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-24" />
</div>
<div>
<FullscreenIcon />
</div>
</div>
<div className="overflow-y-scroll border rounded-b-2xl p-8 pt-4 bg-muted border-t-0 dark:border-zinc-700">
<InlineDocumentSkeleton />
</div>
</div>
);
const PureHitboxLayer = ({
hitboxRef,
result,
setBlock,
}: {
hitboxRef: React.RefObject<HTMLDivElement>;
result: any;
setBlock: (updaterFn: UIBlock | ((currentBlock: UIBlock) => UIBlock)) => void;
}) => {
const handleClick = useCallback(
(event: MouseEvent<HTMLElement>) => {
const boundingBox = event.currentTarget.getBoundingClientRect();
setBlock((block) =>
block.status === 'streaming'
? { ...block, isVisible: true }
: {
...block,
documentId: result.id,
kind: result.kind,
isVisible: true,
boundingBox: {
left: boundingBox.x,
top: boundingBox.y,
width: boundingBox.width,
height: boundingBox.height,
},
},
);
},
[setBlock, result],
);
return (
<div
className="size-full absolute top-0 left-0 rounded-xl z-10"
ref={hitboxRef}
onClick={handleClick}
role="presentation"
aria-hidden="true"
/>
);
};
const HitboxLayer = memo(PureHitboxLayer, (prevProps, nextProps) => {
if (!equal(prevProps.result, nextProps.result)) return false;
return true;
});
const PureDocumentHeader = ({
title,
isStreaming,
}: {
title: string;
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">
<div className="flex flex-row items-start sm:items-center gap-3">
<div className="text-muted-foreground">
{isStreaming ? (
<div className="animate-spin">
<LoaderIcon />
</div>
) : (
<FileIcon />
)}
</div>
<div className="-translate-y-1 sm:translate-y-0 font-medium">{title}</div>
</div>
<div>
<FullscreenIcon />
</div>
</div>
);
const DocumentHeader = memo(PureDocumentHeader, (prevProps, nextProps) => {
if (prevProps.title !== nextProps.title) return false;
if (prevProps.isStreaming !== nextProps.isStreaming) return false;
return true;
});
const DocumentContent = ({ document }: { document: Document }) => {
const { block } = useBlock();
const containerClassName = cn(
'h-[257px] overflow-y-scroll border rounded-b-2xl dark:bg-muted border-t-0 dark:border-zinc-700',
{
'p-4 sm:px-14 sm:py-16': document.kind === 'text',
'p-0': document.kind === 'code',
},
);
const commonProps = {
content: document.content ?? '',
isCurrentVersion: true,
currentVersionIndex: 0,
status: block.status,
saveContent: () => {},
suggestions: [],
};
return (
<div className={containerClassName}>
{document.kind === 'text' ? (
<Editor {...commonProps} />
) : document.kind === 'code' ? (
<div className="flex flex-1 relative w-full">
<div className="absolute inset-0">
<CodeEditor {...commonProps} />
</div>
</div>
) : null}
</div>
);
};

View file

@ -13,3 +13,17 @@ export const DocumentSkeleton = () => {
</div> </div>
); );
}; };
export const InlineDocumentSkeleton = () => {
return (
<div className="flex flex-col gap-4 w-full">
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-48" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-3/4" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-1/2" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-64" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-40" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-36" />
<div className="animate-pulse rounded-lg h-4 bg-muted-foreground/20 w-64" />
</div>
);
};

View file

@ -1,8 +1,9 @@
import { memo, type SetStateAction } from 'react'; import { memo } from 'react';
import type { BlockKind, UIBlock } from './block'; import type { BlockKind } from './block';
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons'; import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useBlock } from '@/hooks/use-block';
const getActionText = ( const getActionText = (
type: 'create' | 'update' | 'request-suggestions', type: 'create' | 'update' | 'request-suggestions',
@ -25,17 +26,16 @@ const getActionText = (
interface DocumentToolResultProps { interface DocumentToolResultProps {
type: 'create' | 'update' | 'request-suggestions'; type: 'create' | 'update' | 'request-suggestions';
result: { id: string; title: string; kind: BlockKind }; result: { id: string; title: string; kind: BlockKind };
block: UIBlock;
setBlock: (value: SetStateAction<UIBlock>) => void;
isReadonly: boolean; isReadonly: boolean;
} }
function PureDocumentToolResult({ function PureDocumentToolResult({
type, type,
result, result,
setBlock,
isReadonly, isReadonly,
}: DocumentToolResultProps) { }: DocumentToolResultProps) {
const { setBlock } = useBlock();
return ( return (
<button <button
type="button" type="button"
@ -89,16 +89,16 @@ export const DocumentToolResult = memo(PureDocumentToolResult, () => true);
interface DocumentToolCallProps { interface DocumentToolCallProps {
type: 'create' | 'update' | 'request-suggestions'; type: 'create' | 'update' | 'request-suggestions';
args: { title: string }; args: { title: string };
setBlock: (value: SetStateAction<UIBlock>) => void;
isReadonly: boolean; isReadonly: boolean;
} }
function PureDocumentToolCall({ function PureDocumentToolCall({
type, type,
args, args,
setBlock,
isReadonly, isReadonly,
}: DocumentToolCallProps) { }: DocumentToolCallProps) {
const { setBlock } = useBlock();
return ( return (
<button <button
type="button" type="button"

View file

@ -1102,3 +1102,20 @@ export const ImageIcon = ({ size = 16 }: { size?: number }) => {
</svg> </svg>
); );
}; };
export const FullscreenIcon = ({ size = 16 }: { size?: number }) => (
<svg
height={size}
strokeLinejoin="round"
viewBox="0 0 16 16"
width={size}
style={{ color: 'currentcolor' }}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 5.25V6H2.5V5.25V2.5H5.25H6V1H5.25H2C1.44772 1 1 1.44772 1 2V5.25ZM5.25 14.9994H6V13.4994H5.25H2.5V10.7494V9.99939H1V10.7494V13.9994C1 14.5517 1.44772 14.9994 2 14.9994H5.25ZM15 10V10.75V14C15 14.5523 14.5523 15 14 15H10.75H10V13.5H10.75H13.5V10.75V10H15ZM10.75 1H10V2.5H10.75H13.5V5.25V6H15V5.25V2C15 1.44772 14.5523 1 14 1H10.75Z"
fill="currentColor"
></path>
</svg>
);

View file

@ -1,10 +1,9 @@
import Link from 'next/link'; import Link from 'next/link';
import React, { memo } from 'react'; import React, { memo, useMemo, useState } from 'react';
import ReactMarkdown, { type Components } from 'react-markdown'; import ReactMarkdown, { type Components } from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { CodeBlock } from './code-block'; import { CodeBlock } from './code-block';
const NonMemoizedMarkdown = ({ children }: { children: string }) => {
const components: Partial<Components> = { const components: Partial<Components> = {
// @ts-expect-error // @ts-expect-error
code: CodeBlock, code: CodeBlock,
@ -94,8 +93,11 @@ const NonMemoizedMarkdown = ({ children }: { children: string }) => {
}, },
}; };
const remarkPlugins = [remarkGfm];
const NonMemoizedMarkdown = ({ children }: { children: string }) => {
return ( return (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}> <ReactMarkdown remarkPlugins={remarkPlugins} components={components}>
{children} {children}
</ReactMarkdown> </ReactMarkdown>
); );

View file

@ -2,12 +2,11 @@
import type { ChatRequestOptions, Message } from 'ai'; import type { ChatRequestOptions, Message } from 'ai';
import cx from 'classnames'; import cx from 'classnames';
import { motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { memo, useState, type Dispatch, type SetStateAction } from 'react'; import { memo, useMemo, useState } from 'react';
import type { Vote } from '@/lib/db/schema'; import type { Vote } from '@/lib/db/schema';
import type { UIBlock } from './block';
import { DocumentToolCall, DocumentToolResult } from './document'; import { DocumentToolCall, DocumentToolResult } from './document';
import { PencilEditIcon, SparklesIcon } from './icons'; import { PencilEditIcon, SparklesIcon } from './icons';
import { Markdown } from './markdown'; import { Markdown } from './markdown';
@ -19,12 +18,11 @@ import { cn } from '@/lib/utils';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
import { MessageEditor } from './message-editor'; import { MessageEditor } from './message-editor';
import { DocumentPreview } from './document-preview';
const PurePreviewMessage = ({ const PurePreviewMessage = ({
chatId, chatId,
message, message,
block,
setBlock,
vote, vote,
isLoading, isLoading,
setMessages, setMessages,
@ -33,8 +31,6 @@ const PurePreviewMessage = ({
}: { }: {
chatId: string; chatId: string;
message: Message; message: Message;
block: UIBlock;
setBlock: Dispatch<SetStateAction<UIBlock>>;
vote: Vote | undefined; vote: Vote | undefined;
isLoading: boolean; isLoading: boolean;
setMessages: ( setMessages: (
@ -48,6 +44,7 @@ const PurePreviewMessage = ({
const [mode, setMode] = useState<'view' | 'edit'>('view'); const [mode, setMode] = useState<'view' | 'edit'>('view');
return ( return (
<AnimatePresence>
<motion.div <motion.div
className="w-full mx-auto max-w-3xl px-4 group/message" className="w-full mx-auto max-w-3xl px-4 group/message"
initial={{ y: 5, opacity: 0 }} initial={{ y: 5, opacity: 0 }}
@ -138,27 +135,20 @@ const PurePreviewMessage = ({
{toolName === 'getWeather' ? ( {toolName === 'getWeather' ? (
<Weather weatherAtLocation={result} /> <Weather weatherAtLocation={result} />
) : toolName === 'createDocument' ? ( ) : toolName === 'createDocument' ? (
<DocumentToolResult <DocumentPreview
type="create"
result={result}
block={block}
setBlock={setBlock}
isReadonly={isReadonly} isReadonly={isReadonly}
result={result}
/> />
) : toolName === 'updateDocument' ? ( ) : toolName === 'updateDocument' ? (
<DocumentToolResult <DocumentToolResult
type="update" type="update"
result={result} result={result}
block={block}
setBlock={setBlock}
isReadonly={isReadonly} isReadonly={isReadonly}
/> />
) : toolName === 'requestSuggestions' ? ( ) : toolName === 'requestSuggestions' ? (
<DocumentToolResult <DocumentToolResult
type="request-suggestions" type="request-suggestions"
result={result} result={result}
block={block}
setBlock={setBlock}
isReadonly={isReadonly} isReadonly={isReadonly}
/> />
) : ( ) : (
@ -177,24 +167,17 @@ const PurePreviewMessage = ({
{toolName === 'getWeather' ? ( {toolName === 'getWeather' ? (
<Weather /> <Weather />
) : toolName === 'createDocument' ? ( ) : toolName === 'createDocument' ? (
<DocumentToolCall <DocumentPreview isReadonly={isReadonly} args={args} />
type="create"
args={args}
setBlock={setBlock}
isReadonly={isReadonly}
/>
) : toolName === 'updateDocument' ? ( ) : toolName === 'updateDocument' ? (
<DocumentToolCall <DocumentToolCall
type="update" type="update"
args={args} args={args}
setBlock={setBlock}
isReadonly={isReadonly} isReadonly={isReadonly}
/> />
) : toolName === 'requestSuggestions' ? ( ) : toolName === 'requestSuggestions' ? (
<DocumentToolCall <DocumentToolCall
type="request-suggestions" type="request-suggestions"
args={args} args={args}
setBlock={setBlock}
isReadonly={isReadonly} isReadonly={isReadonly}
/> />
) : null} ) : null}
@ -216,6 +199,7 @@ const PurePreviewMessage = ({
</div> </div>
</div> </div>
</motion.div> </motion.div>
</AnimatePresence>
); );
}; };

View file

@ -2,15 +2,12 @@ import { ChatRequestOptions, Message } from 'ai';
import { PreviewMessage, ThinkingMessage } from './message'; import { PreviewMessage, ThinkingMessage } from './message';
import { useScrollToBottom } from './use-scroll-to-bottom'; import { useScrollToBottom } from './use-scroll-to-bottom';
import { Overview } from './overview'; import { Overview } from './overview';
import { UIBlock } from './block'; import { memo } from 'react';
import { Dispatch, memo, SetStateAction } from 'react';
import { Vote } from '@/lib/db/schema'; import { Vote } from '@/lib/db/schema';
import equal from 'fast-deep-equal'; import equal from 'fast-deep-equal';
interface MessagesProps { interface MessagesProps {
chatId: string; chatId: string;
block: UIBlock;
setBlock: Dispatch<SetStateAction<UIBlock>>;
isLoading: boolean; isLoading: boolean;
votes: Array<Vote> | undefined; votes: Array<Vote> | undefined;
messages: Array<Message>; messages: Array<Message>;
@ -21,12 +18,11 @@ interface MessagesProps {
chatRequestOptions?: ChatRequestOptions, chatRequestOptions?: ChatRequestOptions,
) => Promise<string | null | undefined>; ) => Promise<string | null | undefined>;
isReadonly: boolean; isReadonly: boolean;
isBlockVisible: boolean;
} }
function PureMessages({ function PureMessages({
chatId, chatId,
block,
setBlock,
isLoading, isLoading,
votes, votes,
messages, messages,
@ -42,15 +38,13 @@ function PureMessages({
ref={messagesContainerRef} ref={messagesContainerRef}
className="flex flex-col min-w-0 gap-6 flex-1 overflow-y-scroll pt-4" className="flex flex-col min-w-0 gap-6 flex-1 overflow-y-scroll pt-4"
> >
{messages.length === 0 && <Overview />} {/* {messages.length === 0 && <Overview />} */}
{messages.map((message, index) => ( {messages.map((message, index) => (
<PreviewMessage <PreviewMessage
key={message.id} key={message.id}
chatId={chatId} chatId={chatId}
message={message} message={message}
block={block}
setBlock={setBlock}
isLoading={isLoading && messages.length - 1 === index} isLoading={isLoading && messages.length - 1 === index}
vote={ vote={
votes votes
@ -76,6 +70,8 @@ function PureMessages({
} }
export const Messages = memo(PureMessages, (prevProps, nextProps) => { export const Messages = memo(PureMessages, (prevProps, nextProps) => {
if (prevProps.isBlockVisible && nextProps.isBlockVisible) return true;
if (prevProps.isLoading !== nextProps.isLoading) return false; if (prevProps.isLoading !== nextProps.isLoading) return false;
if (prevProps.isLoading && nextProps.isLoading) return false; if (prevProps.isLoading && nextProps.isLoading) return false;
if (prevProps.messages.length !== nextProps.messages.length) return false; if (prevProps.messages.length !== nextProps.messages.length) return false;

View file

@ -21,9 +21,9 @@ function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) {
action: 'What is the weather in San Francisco?', action: 'What is the weather in San Francisco?',
}, },
{ {
title: 'Help me write code to', title: 'Write code that',
label: 'demonstrate the djikstra algorithm!', label: `demonstrates djikstra's algorithm!`,
action: 'Help me write code to demonstrate the djikstra algorithm!', action: `Write code that demonstrates djikstra's algorithm!`,
}, },
]; ];

View file

@ -1,132 +0,0 @@
import type { JSONValue } from 'ai';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import { useSWRConfig } from 'swr';
import type { Suggestion } from '@/lib/db/schema';
import type { BlockKind, UIBlock } from './block';
import { useUserMessageId } from '@/hooks/use-user-message-id';
type StreamingDelta = {
type:
| 'text-delta'
| 'code-delta'
| 'title'
| 'id'
| 'suggestion'
| 'clear'
| 'finish'
| 'user-message-id'
| 'kind';
content: string | Suggestion;
};
export function useBlockStream({
streamingData,
setBlock,
}: {
streamingData: JSONValue[] | undefined;
setBlock: Dispatch<SetStateAction<UIBlock>>;
}) {
const { mutate } = useSWRConfig();
const [optimisticSuggestions, setOptimisticSuggestions] = useState<
Array<Suggestion>
>([]);
const { setUserMessageIdFromServer } = useUserMessageId();
useEffect(() => {
if (optimisticSuggestions && optimisticSuggestions.length > 0) {
const [optimisticSuggestion] = optimisticSuggestions;
const url = `/api/suggestions?documentId=${optimisticSuggestion.documentId}`;
mutate(url, optimisticSuggestions, false);
}
}, [optimisticSuggestions, mutate]);
useEffect(() => {
const mostRecentDelta = streamingData?.at(-1);
if (!mostRecentDelta) return;
const delta = mostRecentDelta as StreamingDelta;
if (delta.type === 'user-message-id') {
setUserMessageIdFromServer(delta.content as string);
return;
}
setBlock((draftBlock) => {
switch (delta.type) {
case 'id':
return {
...draftBlock,
documentId: delta.content as string,
};
case 'title':
return {
...draftBlock,
title: delta.content as string,
};
case 'kind':
return {
...draftBlock,
kind: delta.content as BlockKind,
};
case 'text-delta':
return {
...draftBlock,
content: draftBlock.content + (delta.content as string),
isVisible:
draftBlock.status === 'streaming' &&
draftBlock.content.length > 200 &&
draftBlock.content.length < 250
? true
: draftBlock.isVisible,
status: 'streaming',
};
case 'code-delta':
return {
...draftBlock,
content: delta.content as string,
isVisible:
draftBlock.status === 'streaming' &&
draftBlock.content.length > 20 &&
draftBlock.content.length < 30
? true
: draftBlock.isVisible,
status: 'streaming',
};
case 'suggestion':
setTimeout(() => {
setOptimisticSuggestions((currentSuggestions) => [
...currentSuggestions,
delta.content as Suggestion,
]);
}, 0);
return draftBlock;
case 'clear':
return {
...draftBlock,
content: '',
status: 'streaming',
};
case 'finish':
return {
...draftBlock,
status: 'idle',
};
default:
return draftBlock;
}
});
}, [streamingData, setBlock, setUserMessageIdFromServer]);
}

View file

@ -12,20 +12,21 @@ import { getDocumentTimestampByIndex } from '@/lib/utils';
import type { UIBlock } from './block'; import type { UIBlock } from './block';
import { LoaderIcon } from './icons'; import { LoaderIcon } from './icons';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { useBlock } from '@/hooks/use-block';
interface VersionFooterProps { interface VersionFooterProps {
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 = ({
block,
handleVersionChange, handleVersionChange,
documents, documents,
currentVersionIndex, currentVersionIndex,
}: VersionFooterProps) => { }: VersionFooterProps) => {
const { block } = useBlock();
const { width } = useWindowSize(); const { width } = useWindowSize();
const isMobile = width < 768; const isMobile = width < 768;

68
hooks/use-block.ts Normal file
View file

@ -0,0 +1,68 @@
'use client';
import { UIBlock } from '@/components/block';
import { useCallback, useMemo } from 'react';
import useSWR from 'swr';
export const initialBlockData: UIBlock = {
documentId: 'init',
content: '',
kind: 'text',
title: '',
status: 'idle',
isVisible: false,
boundingBox: {
top: 0,
left: 0,
width: 0,
height: 0,
},
};
// Add type for selector function
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],
);
return useMemo(() => ({ block, setBlock }), [block, setBlock]);
}

View file

@ -1,7 +1,7 @@
export const blocksPrompt = ` 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. 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.
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 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.
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 blocks tools: \`createDocument\` and \`updateDocument\`, which render content on a blocks beside the conversation.

View file

@ -37,7 +37,7 @@
"@vercel/analytics": "^1.3.1", "@vercel/analytics": "^1.3.1",
"@vercel/blob": "^0.24.1", "@vercel/blob": "^0.24.1",
"@vercel/postgres": "^0.10.0", "@vercel/postgres": "^0.10.0",
"ai": "4.0.10", "ai": "4.0.20",
"bcrypt-ts": "^5.0.2", "bcrypt-ts": "^5.0.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",

71
pnpm-lock.yaml generated
View file

@ -66,8 +66,8 @@ importers:
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0 version: 0.10.0
ai: ai:
specifier: 4.0.10 specifier: 4.0.20
version: 4.0.10(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8) version: 4.0.20(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)
bcrypt-ts: bcrypt-ts:
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.2 version: 5.0.2
@ -255,12 +255,25 @@ packages:
zod: zod:
optional: true optional: true
'@ai-sdk/provider-utils@2.0.4':
resolution: {integrity: sha512-GMhcQCZbwM6RoZCri0MWeEWXRt/T+uCxsmHEsTwNvEH3GDjNzchfX25C8ftry2MeEOOn6KfqCLSKomcgK6RoOg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
peerDependenciesMeta:
zod:
optional: true
'@ai-sdk/provider@1.0.1': '@ai-sdk/provider@1.0.1':
resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==} resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@ai-sdk/react@1.0.3': '@ai-sdk/provider@1.0.2':
resolution: {integrity: sha512-Mak7qIRlbgtP4I7EFoNKRIQTlABJHhgwrN8SV2WKKdmsfWK2RwcubQWz1hp88cQ0bpF6KxxjSY1UUnS/S9oR5g==} resolution: {integrity: sha512-YYtP6xWQyaAf5LiWLJ+ycGTOeBLWrED7LUrvc+SQIWhGaneylqbaGsyQL7VouQUeQ4JZ1qKYZuhmi3W56HADPA==}
engines: {node: '>=18'}
'@ai-sdk/react@1.0.6':
resolution: {integrity: sha512-8Hkserq0Ge6AEi7N4hlv2FkfglAGbkoAXEZ8YSp255c3PbnZz6+/5fppw+aROmZMOfNwallSRuy1i/iPa2rBpQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
@ -271,8 +284,8 @@ packages:
zod: zod:
optional: true optional: true
'@ai-sdk/ui-utils@1.0.2': '@ai-sdk/ui-utils@1.0.5':
resolution: {integrity: sha512-hHrUdeThGHu/rsGZBWQ9PjrAU9Htxgbo9MFyR5B/aWoNbBeXn1HLMY1+uMEnXL5pRPlmyVRjgIavWg7UgeNDOw==} resolution: {integrity: sha512-DGJSbDf+vJyWmFNexSPUsS1AAy7gtsmFmoSyNbNbJjwl9hRIf2dknfA1V0ahx6pg3NNklNYFm53L8Nphjovfvg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
@ -1613,8 +1626,8 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
ai@4.0.10: ai@4.0.20:
resolution: {integrity: sha512-40GaEGLbp7if1F50zp3Kr03vcqyGS8svyJWpbkgec7G5Ik2rEtnbDWiUoOJuAVqgP5/iy4NgZQfvX3jRmOyQrw==} resolution: {integrity: sha512-dYevYKtREcjSVopBDFWVNca7WJEI1p9Vr9eo7V7fZHzi2vXGDyEa2WYatjFbpR6z6gpVAxKHsof8EoN+B1IAsA==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
@ -2876,6 +2889,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true hasBin: true
nanoid@3.3.8:
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@5.0.8: nanoid@5.0.8:
resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==}
engines: {node: ^18 || >=20} engines: {node: ^18 || >=20}
@ -3748,24 +3766,37 @@ snapshots:
optionalDependencies: optionalDependencies:
zod: 3.23.8 zod: 3.23.8
'@ai-sdk/provider-utils@2.0.4(zod@3.23.8)':
dependencies:
'@ai-sdk/provider': 1.0.2
eventsource-parser: 3.0.0
nanoid: 3.3.8
secure-json-parse: 2.7.0
optionalDependencies:
zod: 3.23.8
'@ai-sdk/provider@1.0.1': '@ai-sdk/provider@1.0.1':
dependencies: dependencies:
json-schema: 0.4.0 json-schema: 0.4.0
'@ai-sdk/react@1.0.3(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)': '@ai-sdk/provider@1.0.2':
dependencies: dependencies:
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) json-schema: 0.4.0
'@ai-sdk/ui-utils': 1.0.2(zod@3.23.8)
'@ai-sdk/react@1.0.6(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)':
dependencies:
'@ai-sdk/provider-utils': 2.0.4(zod@3.23.8)
'@ai-sdk/ui-utils': 1.0.5(zod@3.23.8)
swr: 2.2.5(react@19.0.0-rc-45804af1-20241021) swr: 2.2.5(react@19.0.0-rc-45804af1-20241021)
throttleit: 2.1.0 throttleit: 2.1.0
optionalDependencies: optionalDependencies:
react: 19.0.0-rc-45804af1-20241021 react: 19.0.0-rc-45804af1-20241021
zod: 3.23.8 zod: 3.23.8
'@ai-sdk/ui-utils@1.0.2(zod@3.23.8)': '@ai-sdk/ui-utils@1.0.5(zod@3.23.8)':
dependencies: dependencies:
'@ai-sdk/provider': 1.0.1 '@ai-sdk/provider': 1.0.2
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) '@ai-sdk/provider-utils': 2.0.4(zod@3.23.8)
zod-to-json-schema: 3.23.5(zod@3.23.8) zod-to-json-schema: 3.23.5(zod@3.23.8)
optionalDependencies: optionalDependencies:
zod: 3.23.8 zod: 3.23.8
@ -4865,12 +4896,12 @@ snapshots:
acorn@8.14.0: {} acorn@8.14.0: {}
ai@4.0.10(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8): ai@4.0.20(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8):
dependencies: dependencies:
'@ai-sdk/provider': 1.0.1 '@ai-sdk/provider': 1.0.2
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) '@ai-sdk/provider-utils': 2.0.4(zod@3.23.8)
'@ai-sdk/react': 1.0.3(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8) '@ai-sdk/react': 1.0.6(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)
'@ai-sdk/ui-utils': 1.0.2(zod@3.23.8) '@ai-sdk/ui-utils': 1.0.5(zod@3.23.8)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0 jsondiffpatch: 0.6.0
zod-to-json-schema: 3.23.5(zod@3.23.8) zod-to-json-schema: 3.23.5(zod@3.23.8)
@ -6484,6 +6515,8 @@ snapshots:
nanoid@3.3.7: {} nanoid@3.3.7: {}
nanoid@3.3.8: {}
nanoid@5.0.8: {} nanoid@5.0.8: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}