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

@ -31,6 +31,9 @@ import { BlockCloseButton } from './block-close-button';
import { BlockMessages } from './block-messages';
import { CodeEditor } from './code-editor';
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';
@ -65,8 +68,6 @@ function PureBlock({
attachments,
setAttachments,
append,
block,
setBlock,
messages,
setMessages,
reload,
@ -80,8 +81,6 @@ function PureBlock({
stop: () => void;
attachments: Array<Attachment>;
setAttachments: Dispatch<SetStateAction<Array<Attachment>>>;
block: UIBlock;
setBlock: Dispatch<SetStateAction<UIBlock>>;
messages: Array<Message>;
setMessages: Dispatch<SetStateAction<Array<Message>>>;
votes: Array<Vote> | undefined;
@ -100,12 +99,14 @@ function PureBlock({
) => Promise<string | null | undefined>;
isReadonly: boolean;
}) {
const { block, setBlock } = useBlock();
const {
data: documents,
isLoading: isDocumentsFetching,
mutate: mutateDocuments,
} = useSWR<Array<Document>>(
block && block.status !== 'streaming'
block.documentId !== 'init' && block.status !== 'streaming'
? `/api/document?id=${block.documentId}`
: null,
fetcher,
@ -128,6 +129,8 @@ function PureBlock({
[],
);
const { open: isSidebarOpen } = useSidebar();
useEffect(() => {
if (documents && documents.length > 0) {
const mostRecentDocument = documents.at(-1);
@ -260,274 +263,302 @@ function PureBlock({
const isMobile = windowWidth ? windowWidth < 768 : false;
return (
<motion.div
className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-muted"
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { delay: 0.4 } }}
>
{!isMobile && (
<AnimatePresence>
{block.isVisible && (
<motion.div
className="relative w-[400px] bg-muted dark:bg-background h-dvh shrink-0"
initial={{ opacity: 0, x: 10, scale: 1 }}
animate={{
opacity: 1,
x: 0,
scale: 1,
transition: {
delay: 0.2,
type: 'spring',
stiffness: 200,
damping: 30,
},
}}
exit={{
opacity: 0,
x: 0,
scale: 0.95,
transition: { delay: 0 },
}}
className="flex flex-row h-dvh w-dvw fixed top-0 left-0 z-50 bg-transparent"
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { delay: 0.4 } }}
>
<AnimatePresence>
{!isCurrentVersion && (
<motion.div
className="left-0 absolute h-dvh w-[400px] top-0 bg-zinc-900/50 z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
<div className="flex flex-col h-full justify-between items-center gap-4">
<BlockMessages
chatId={chatId}
block={block}
isLoading={isLoading}
setBlock={setBlock}
votes={votes}
messages={messages}
setMessages={setMessages}
reload={reload}
isReadonly={isReadonly}
{!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,
}}
/>
)}
<form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
<MultimodalInput
chatId={chatId}
input={input}
setInput={setInput}
handleSubmit={handleSubmit}
isLoading={isLoading}
stop={stop}
attachments={attachments}
setAttachments={setAttachments}
messages={messages}
append={append}
className="bg-background dark:bg-muted"
setMessages={setMessages}
/>
</form>
</div>
</motion.div>
)}
<motion.div
className="fixed dark:bg-muted bg-background h-dvh flex flex-col overflow-y-scroll"
initial={
isMobile
? {
opacity: 0,
x: 0,
y: 0,
width: windowWidth,
height: windowHeight,
borderRadius: 50,
}
: {
opacity: 0,
x: block.boundingBox.left,
y: block.boundingBox.top,
height: block.boundingBox.height,
width: block.boundingBox.width,
borderRadius: 50,
}
}
animate={
isMobile
? {
{!isMobile && (
<motion.div
className="relative w-[400px] bg-muted dark:bg-background h-dvh shrink-0"
initial={{ opacity: 0, x: 10, scale: 1 }}
animate={{
opacity: 1,
x: 0,
y: 0,
width: windowWidth,
height: '100dvh',
borderRadius: 0,
scale: 1,
transition: {
delay: 0,
delay: 0.2,
type: 'spring',
stiffness: 200,
damping: 30,
},
}
: {
opacity: 1,
x: 400,
y: 0,
height: windowHeight,
width: windowWidth ? windowWidth - 400 : 'calc(100dvw-400px)',
borderRadius: 0,
transition: {
delay: 0,
type: 'spring',
stiffness: 200,
damping: 30,
},
}
}
exit={{
opacity: 0,
scale: 0.5,
transition: {
delay: 0.1,
type: 'spring',
stiffness: 600,
damping: 30,
},
}}
>
<div className="p-2 flex flex-row justify-between items-start">
<div className="flex flex-row gap-4 items-start">
<BlockCloseButton setBlock={setBlock} />
}}
exit={{
opacity: 0,
x: 0,
scale: 1,
transition: { duration: 0 },
}}
>
<AnimatePresence>
{!isCurrentVersion && (
<motion.div
className="left-0 absolute h-dvh w-[400px] top-0 bg-zinc-900/50 z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
<div className="flex flex-col">
<div className="font-medium">
{document?.title ?? block.title}
<div className="flex flex-col h-full justify-between items-center gap-4">
<BlockMessages
chatId={chatId}
isLoading={isLoading}
votes={votes}
messages={messages}
setMessages={setMessages}
reload={reload}
isReadonly={isReadonly}
blockStatus={block.status}
/>
<form className="flex flex-row gap-2 relative items-end w-full px-4 pb-4">
<MultimodalInput
chatId={chatId}
input={input}
setInput={setInput}
handleSubmit={handleSubmit}
isLoading={isLoading}
stop={stop}
attachments={attachments}
setAttachments={setAttachments}
messages={messages}
append={append}
className="bg-background dark:bg-muted"
setMessages={setMessages}
/>
</form>
</div>
</motion.div>
)}
<motion.div
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={
isMobile
? {
opacity: 1,
x: block.boundingBox.left,
y: block.boundingBox.top,
height: block.boundingBox.height,
width: block.boundingBox.width,
borderRadius: 50,
}
: {
opacity: 1,
x: block.boundingBox.left,
y: block.boundingBox.top,
height: block.boundingBox.height,
width: block.boundingBox.width,
borderRadius: 50,
}
}
animate={
isMobile
? {
opacity: 1,
x: 0,
y: 0,
height: windowHeight,
width: windowWidth ? windowWidth : 'calc(100dvw)',
borderRadius: 0,
transition: {
delay: 0,
type: 'spring',
stiffness: 200,
damping: 30,
duration: 5000,
},
}
: {
opacity: 1,
x: 400,
y: 0,
height: windowHeight,
width: windowWidth
? windowWidth - 400
: 'calc(100dvw-400px)',
borderRadius: 0,
transition: {
delay: 0,
type: 'spring',
stiffness: 200,
damping: 30,
duration: 5000,
},
}
}
exit={{
opacity: 0,
scale: 0.5,
transition: {
delay: 0.1,
type: 'spring',
stiffness: 600,
damping: 30,
},
}}
>
<div className="p-2 flex flex-row justify-between items-start">
<div className="flex flex-row gap-4 items-start">
<BlockCloseButton />
<div className="flex flex-col">
<div className="font-medium">
{document?.title ?? block.title}
</div>
{isContentDirty ? (
<div className="text-sm text-muted-foreground">
Saving changes...
</div>
) : document ? (
<div className="text-sm text-muted-foreground">
{`Updated ${formatDistance(
new Date(document.createdAt),
new Date(),
{
addSuffix: true,
},
)}`}
</div>
) : (
<div className="w-32 h-3 mt-2 bg-muted-foreground/20 rounded-md animate-pulse" />
)}
</div>
</div>
{isContentDirty ? (
<div className="text-sm text-muted-foreground">
Saving changes...
</div>
) : document ? (
<div className="text-sm text-muted-foreground">
{`Updated ${formatDistance(
new Date(document.createdAt),
new Date(),
{
addSuffix: true,
},
)}`}
</div>
) : (
<div className="w-32 h-3 mt-2 bg-muted-foreground/20 rounded-md animate-pulse" />
)}
</div>
</div>
<BlockActions
block={block}
currentVersionIndex={currentVersionIndex}
handleVersionChange={handleVersionChange}
isCurrentVersion={isCurrentVersion}
mode={mode}
setConsoleOutputs={setConsoleOutputs}
/>
</div>
<div
className={cn(
'prose dark:prose-invert 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-8 md:p-20 px-4': block.kind === 'text',
},
)}
>
<div
className={cn('flex flex-row', {
'': block.kind === 'code',
'mx-auto max-w-[600px]': block.kind === 'text',
})}
>
{isDocumentsFetching && !block.content ? (
<DocumentSkeleton />
) : block.kind === 'code' ? (
<CodeEditor
content={
isCurrentVersion
? block.content
: getDocumentContentById(currentVersionIndex)
}
isCurrentVersion={isCurrentVersion}
<BlockActions
block={block}
currentVersionIndex={currentVersionIndex}
suggestions={suggestions ?? []}
status={block.status}
saveContent={saveContent}
handleVersionChange={handleVersionChange}
isCurrentVersion={isCurrentVersion}
mode={mode}
setConsoleOutputs={setConsoleOutputs}
/>
) : block.kind === 'text' ? (
mode === 'edit' ? (
<Editor
content={
isCurrentVersion
? block.content
: getDocumentContentById(currentVersionIndex)
}
isCurrentVersion={isCurrentVersion}
currentVersionIndex={currentVersionIndex}
status={block.status}
saveContent={saveContent}
suggestions={isCurrentVersion ? (suggestions ?? []) : []}
/>
) : (
<DiffView
oldContent={getDocumentContentById(currentVersionIndex - 1)}
newContent={getDocumentContentById(currentVersionIndex)}
/>
)
) : null}
</div>
{suggestions ? (
<div className="md:hidden h-dvh w-12 shrink-0" />
) : null}
<div
className={cn(
'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-8 md:p-20 px-4': block.kind === 'text',
},
)}
>
<div
className={cn('flex flex-row', {
'': block.kind === 'code',
'mx-auto max-w-[600px]': block.kind === 'text',
})}
>
{isDocumentsFetching && !block.content ? (
<DocumentSkeleton />
) : block.kind === 'code' ? (
<CodeEditor
content={
isCurrentVersion
? block.content
: getDocumentContentById(currentVersionIndex)
}
isCurrentVersion={isCurrentVersion}
currentVersionIndex={currentVersionIndex}
suggestions={suggestions ?? []}
status={block.status}
saveContent={saveContent}
/>
) : block.kind === 'text' ? (
mode === 'edit' ? (
<Editor
content={
isCurrentVersion
? block.content
: getDocumentContentById(currentVersionIndex)
}
isCurrentVersion={isCurrentVersion}
currentVersionIndex={currentVersionIndex}
status={block.status}
saveContent={saveContent}
suggestions={isCurrentVersion ? (suggestions ?? []) : []}
/>
) : (
<DiffView
oldContent={getDocumentContentById(
currentVersionIndex - 1,
)}
newContent={getDocumentContentById(currentVersionIndex)}
/>
)
) : null}
{suggestions ? (
<div className="md:hidden h-dvh w-12 shrink-0" />
) : null}
<AnimatePresence>
{isCurrentVersion && (
<Toolbar
isToolbarVisible={isToolbarVisible}
setIsToolbarVisible={setIsToolbarVisible}
append={append}
isLoading={isLoading}
stop={stop}
setMessages={setMessages}
blockKind={block.kind}
/>
)}
</AnimatePresence>
</div>
</div>
<AnimatePresence>
{isCurrentVersion && (
<Toolbar
isToolbarVisible={isToolbarVisible}
setIsToolbarVisible={setIsToolbarVisible}
append={append}
isLoading={isLoading}
stop={stop}
setMessages={setMessages}
blockKind={block.kind}
{!isCurrentVersion && (
<VersionFooter
currentVersionIndex={currentVersionIndex}
documents={documents}
handleVersionChange={handleVersionChange}
/>
)}
</AnimatePresence>
</div>
</div>
<AnimatePresence>
{!isCurrentVersion && (
<VersionFooter
block={block}
currentVersionIndex={currentVersionIndex}
documents={documents}
handleVersionChange={handleVersionChange}
/>
)}
</AnimatePresence>
<AnimatePresence>
<Console
consoleOutputs={consoleOutputs}
setConsoleOutputs={setConsoleOutputs}
/>
</AnimatePresence>
</motion.div>
</motion.div>
<AnimatePresence>
<Console
consoleOutputs={consoleOutputs}
setConsoleOutputs={setConsoleOutputs}
/>
</AnimatePresence>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
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;
});