-
-
+ }}
+ exit={{
+ opacity: 0,
+ x: 0,
+ scale: 1,
+ transition: { duration: 0 },
+ }}
+ >
+
+ {!isCurrentVersion && (
+
+ )}
+
-
-
- {document?.title ?? block.title}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {document?.title ?? block.title}
+
+
+ {isContentDirty ? (
+
+ Saving changes...
+
+ ) : document ? (
+
+ {`Updated ${formatDistance(
+ new Date(document.createdAt),
+ new Date(),
+ {
+ addSuffix: true,
+ },
+ )}`}
+
+ ) : (
+
+ )}
+
- {isContentDirty ? (
-
- Saving changes...
-
- ) : document ? (
-
- {`Updated ${formatDistance(
- new Date(document.createdAt),
- new Date(),
- {
- addSuffix: true,
- },
- )}`}
-
- ) : (
-
- )}
-
-
-
-
-
-
-
-
- {isDocumentsFetching && !block.content ? (
-
- ) : block.kind === 'code' ? (
-
- ) : block.kind === 'text' ? (
- mode === 'edit' ? (
-
- ) : (
-
- )
- ) : null}
+
- {suggestions ? (
-
- ) : null}
+
+
+ {isDocumentsFetching && !block.content ? (
+
+ ) : block.kind === 'code' ? (
+
+ ) : block.kind === 'text' ? (
+ mode === 'edit' ? (
+
+ ) : (
+
+ )
+ ) : null}
+
+ {suggestions ? (
+
+ ) : null}
+
+
+ {isCurrentVersion && (
+
+ )}
+
+
+
- {isCurrentVersion && (
-
)}
-
-
-
- {!isCurrentVersion && (
-
- )}
-
-
-
-
-
-
-
+
+
+
+
+
+ )}
+
);
}
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;
});
diff --git a/components/chat.tsx b/components/chat.tsx
index 3a56310..a2ea8f5 100644
--- a/components/chat.tsx
+++ b/components/chat.tsx
@@ -2,20 +2,18 @@
import type { Attachment, Message } from 'ai';
import { useChat } from 'ai/react';
-import { AnimatePresence } from 'framer-motion';
import { useState } from 'react';
import useSWR, { useSWRConfig } from 'swr';
-import { useWindowSize } from 'usehooks-ts';
import { ChatHeader } from '@/components/chat-header';
import type { Vote } from '@/lib/db/schema';
import { fetcher } from '@/lib/utils';
-import { Block, type UIBlock } from './block';
-import { BlockStreamHandler } from './block-stream-handler';
+import { Block } from './block';
import { MultimodalInput } from './multimodal-input';
import { Messages } from './messages';
import { VisibilityType } from './visibility-selector';
+import { useBlockSelector } from '@/hooks/use-block';
export function Chat({
id,
@@ -42,40 +40,23 @@ export function Chat({
isLoading,
stop,
reload,
- data: streamingData,
} = useChat({
id,
body: { id, modelId: selectedModelId },
initialMessages,
+ experimental_throttle: 100,
onFinish: () => {
mutate('/api/history');
},
});
- const { width: windowWidth = 1920, height: windowHeight = 1080 } =
- useWindowSize();
-
- const [block, setBlock] = useState
({
- 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>(
`/api/vote?chatId=${id}`,
fetcher,
);
const [attachments, setAttachments] = useState>([]);
+ const isBlockVisible = useBlockSelector((state) => state.isVisible);
return (
<>
@@ -89,14 +70,13 @@ export function Chat({
- {
+ 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;
+}
diff --git a/components/document-preview.tsx b/components/document-preview.tsx
new file mode 100644
index 0000000..fa1d90b
--- /dev/null
+++ b/components/document-preview.tsx
@@ -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
+ >(result ? `/api/document?id=${result.id}` : null, fetcher);
+
+ const previewDocument = useMemo(() => documents?.[0], [documents]);
+ const hitboxRef = useRef(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 (
+
+ );
+ }
+
+ if (args) {
+ return (
+
+ );
+ }
+ }
+
+ if (isDocumentsFetching) {
+ return ;
+ }
+
+ 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 ;
+
+ return (
+
+
+
+
+
+ );
+}
+
+const LoadingSkeleton = () => (
+
+);
+
+const PureHitboxLayer = ({
+ hitboxRef,
+ result,
+ setBlock,
+}: {
+ hitboxRef: React.RefObject;
+ result: any;
+ setBlock: (updaterFn: UIBlock | ((currentBlock: UIBlock) => UIBlock)) => void;
+}) => {
+ const handleClick = useCallback(
+ (event: MouseEvent) => {
+ 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 (
+
+ );
+};
+
+const HitboxLayer = memo(PureHitboxLayer, (prevProps, nextProps) => {
+ if (!equal(prevProps.result, nextProps.result)) return false;
+ return true;
+});
+
+const PureDocumentHeader = ({
+ title,
+ isStreaming,
+}: {
+ title: string;
+ isStreaming: boolean;
+}) => (
+
+
+
+ {isStreaming ? (
+
+
+
+ ) : (
+
+ )}
+
+
{title}
+
+
+
+
+
+);
+
+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 (
+
+ {document.kind === 'text' ? (
+
+ ) : document.kind === 'code' ? (
+
+ ) : null}
+
+ );
+};
diff --git a/components/document-skeleton.tsx b/components/document-skeleton.tsx
index 6e6b03a..7a45794 100644
--- a/components/document-skeleton.tsx
+++ b/components/document-skeleton.tsx
@@ -13,3 +13,17 @@ export const DocumentSkeleton = () => {
);
};
+
+export const InlineDocumentSkeleton = () => {
+ return (
+