diff --git a/app/(chat)/page.tsx b/app/(chat)/page.tsx index 787fefc..fdbbfe9 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -3,6 +3,7 @@ import { cookies } from 'next/headers'; import { Chat } from '@/components/chat'; import { DEFAULT_MODEL_NAME, models } from '@/lib/ai/models'; import { generateUUID } from '@/lib/utils'; +import { DataStreamHandler } from '@/components/data-stream-handler'; export default async function Page() { const id = generateUUID(); @@ -15,13 +16,16 @@ export default async function Page() { DEFAULT_MODEL_NAME; return ( - + <> + + + ); } diff --git a/biome.jsonc b/biome.jsonc index f2bbd27..89447ac 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -45,7 +45,10 @@ "noSvgWithoutTitle": "off", // We do not intend to adhere to this rule "useMediaCaption": "off", // We would need a cultural change to turn this on "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": { "noUselessStringConcat": "warn", // Not in recommended ruleset, turning on manually diff --git a/components/block-close-button.tsx b/components/block-close-button.tsx index 620c151..2d8c820 100644 --- a/components/block-close-button.tsx +++ b/components/block-close-button.tsx @@ -1,23 +1,24 @@ -import { memo, SetStateAction } from 'react'; +import { memo } from 'react'; import { CrossIcon } from './icons'; import { Button } from './ui/button'; -import { UIBlock } from './block'; -import equal from 'fast-deep-equal'; +import { initialBlockData, useBlock } from '@/hooks/use-block'; -interface BlockCloseButtonProps { - setBlock: (value: SetStateAction) => void; -} +function PureBlockCloseButton() { + const { setBlock } = useBlock(); -function PureBlockCloseButton({ setBlock }: BlockCloseButtonProps) { return ( - - Edit message - - )} - -
- {message.content as string} +
+ {message.experimental_attachments && ( +
+ {message.experimental_attachments.map((attachment) => ( + + ))}
-
- )} + )} - {message.content && mode === 'edit' && ( -
-
+ {message.content && mode === 'view' && ( +
+ {message.role === 'user' && !isReadonly && ( + + + + + Edit message + + )} - -
- )} +
+ {message.content as string} +
+
+ )} - {message.toolInvocations && message.toolInvocations.length > 0 && ( -
- {message.toolInvocations.map((toolInvocation) => { - const { toolName, toolCallId, state, args } = toolInvocation; + {message.content && mode === 'edit' && ( +
+
- if (state === 'result') { - const { result } = toolInvocation; + +
+ )} + {message.toolInvocations && message.toolInvocations.length > 0 && ( +
+ {message.toolInvocations.map((toolInvocation) => { + const { toolName, toolCallId, state, args } = toolInvocation; + + if (state === 'result') { + const { result } = toolInvocation; + + return ( +
+ {toolName === 'getWeather' ? ( + + ) : toolName === 'createDocument' ? ( + + ) : toolName === 'updateDocument' ? ( + + ) : toolName === 'requestSuggestions' ? ( + + ) : ( +
{JSON.stringify(result, null, 2)}
+ )} +
+ ); + } return ( -
+
{toolName === 'getWeather' ? ( - + ) : toolName === 'createDocument' ? ( - + ) : toolName === 'updateDocument' ? ( - ) : toolName === 'requestSuggestions' ? ( - - ) : ( -
{JSON.stringify(result, null, 2)}
- )} + ) : null}
); - } - return ( -
- {toolName === 'getWeather' ? ( - - ) : toolName === 'createDocument' ? ( - - ) : toolName === 'updateDocument' ? ( - - ) : toolName === 'requestSuggestions' ? ( - - ) : null} -
- ); - })} -
- )} + })} +
+ )} - {!isReadonly && ( - - )} + {!isReadonly && ( + + )} +
-
- + + ); }; diff --git a/components/messages.tsx b/components/messages.tsx index 43e203f..668e2e1 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -2,15 +2,12 @@ import { ChatRequestOptions, Message } from 'ai'; import { PreviewMessage, ThinkingMessage } from './message'; import { useScrollToBottom } from './use-scroll-to-bottom'; import { Overview } from './overview'; -import { UIBlock } from './block'; -import { Dispatch, memo, SetStateAction } from 'react'; +import { memo } from 'react'; import { Vote } from '@/lib/db/schema'; import equal from 'fast-deep-equal'; interface MessagesProps { chatId: string; - block: UIBlock; - setBlock: Dispatch>; isLoading: boolean; votes: Array | undefined; messages: Array; @@ -21,12 +18,11 @@ interface MessagesProps { chatRequestOptions?: ChatRequestOptions, ) => Promise; isReadonly: boolean; + isBlockVisible: boolean; } function PureMessages({ chatId, - block, - setBlock, isLoading, votes, messages, @@ -42,15 +38,13 @@ function PureMessages({ ref={messagesContainerRef} className="flex flex-col min-w-0 gap-6 flex-1 overflow-y-scroll pt-4" > - {messages.length === 0 && } + {/* {messages.length === 0 && } */} {messages.map((message, index) => ( { + if (prevProps.isBlockVisible && nextProps.isBlockVisible) 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; diff --git a/components/suggested-actions.tsx b/components/suggested-actions.tsx index 7fbc03d..15e9e9a 100644 --- a/components/suggested-actions.tsx +++ b/components/suggested-actions.tsx @@ -21,9 +21,9 @@ function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) { action: 'What is the weather in San Francisco?', }, { - title: 'Help me write code to', - label: 'demonstrate the djikstra algorithm!', - action: 'Help me write code to demonstrate the djikstra algorithm!', + title: 'Write code that', + label: `demonstrates djikstra's algorithm!`, + action: `Write code that demonstrates djikstra's algorithm!`, }, ]; diff --git a/components/use-block-stream.tsx b/components/use-block-stream.tsx deleted file mode 100644 index e9c8892..0000000 --- a/components/use-block-stream.tsx +++ /dev/null @@ -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>; -}) { - const { mutate } = useSWRConfig(); - const [optimisticSuggestions, setOptimisticSuggestions] = useState< - Array - >([]); - - 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]); -} diff --git a/components/version-footer.tsx b/components/version-footer.tsx index 1ca206c..0c08786 100644 --- a/components/version-footer.tsx +++ b/components/version-footer.tsx @@ -12,20 +12,21 @@ import { getDocumentTimestampByIndex } from '@/lib/utils'; import type { UIBlock } from './block'; import { LoaderIcon } from './icons'; import { Button } from './ui/button'; +import { useBlock } from '@/hooks/use-block'; interface VersionFooterProps { - block: UIBlock; handleVersionChange: (type: 'next' | 'prev' | 'toggle' | 'latest') => void; documents: Array | undefined; currentVersionIndex: number; } export const VersionFooter = ({ - block, handleVersionChange, documents, currentVersionIndex, }: VersionFooterProps) => { + const { block } = useBlock(); + const { width } = useWindowSize(); const isMobile = width < 768; diff --git a/hooks/use-block.ts b/hooks/use-block.ts new file mode 100644 index 0000000..32b2677 --- /dev/null +++ b/hooks/use-block.ts @@ -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 = (state: UIBlock) => T; + +export function useBlockSelector(selector: Selector) { + const { data: localBlock } = useSWR('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( + '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]); +} diff --git a/lib/ai/prompts.ts b/lib/ai/prompts.ts index a0d20a2..ae30947 100644 --- a/lib/ai/prompts.ts +++ b/lib/ai/prompts.ts @@ -1,28 +1,28 @@ 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. - **When to use \`createDocument\`:** - - For substantial content (>10 lines) or code - - For content users will likely save/reuse (emails, code, essays, etc.) - - When explicitly requested to create a document - - For when content contains a single code snippet +**When to use \`createDocument\`:** +- For substantial content (>10 lines) or code +- For content users will likely save/reuse (emails, code, essays, etc.) +- When explicitly requested to create a document +- For when content contains a single code snippet - **When NOT to use \`createDocument\`:** - - For informational/explanatory content - - For conversational responses - - When asked to keep it in chat +**When NOT to use \`createDocument\`:** +- For informational/explanatory content +- For conversational responses +- When asked to keep it in chat - **Using \`updateDocument\`:** - - Default to full document rewrites for major changes - - Use targeted updates only for specific, isolated changes - - Follow user instructions for which parts to modify +**Using \`updateDocument\`:** +- Default to full document rewrites for major changes +- Use targeted updates only for specific, isolated changes +- Follow user instructions for which parts to modify - Do not update document right after creating it. Wait for user feedback or request to update it. - `; +Do not update document right after creating it. Wait for user feedback or request to update it. +`; export const regularPrompt = 'You are a friendly assistant! Keep your responses concise and helpful.'; diff --git a/package.json b/package.json index 2ffe24b..5f8f745 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@vercel/analytics": "^1.3.1", "@vercel/blob": "^0.24.1", "@vercel/postgres": "^0.10.0", - "ai": "4.0.10", + "ai": "4.0.20", "bcrypt-ts": "^5.0.2", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edf901b..f8a40e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^0.10.0 version: 0.10.0 ai: - specifier: 4.0.10 - version: 4.0.10(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8) + specifier: 4.0.20 + version: 4.0.20(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8) bcrypt-ts: specifier: ^5.0.2 version: 5.0.2 @@ -255,12 +255,25 @@ packages: zod: 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': resolution: {integrity: sha512-mV+3iNDkzUsZ0pR2jG0sVzU6xtQY5DtSCBy3JFycLp6PwjyLw/iodfL3MwdmMCRJWgs3dadcHejRnMvF9nGTBg==} engines: {node: '>=18'} - '@ai-sdk/react@1.0.3': - resolution: {integrity: sha512-Mak7qIRlbgtP4I7EFoNKRIQTlABJHhgwrN8SV2WKKdmsfWK2RwcubQWz1hp88cQ0bpF6KxxjSY1UUnS/S9oR5g==} + '@ai-sdk/provider@1.0.2': + 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'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -271,8 +284,8 @@ packages: zod: optional: true - '@ai-sdk/ui-utils@1.0.2': - resolution: {integrity: sha512-hHrUdeThGHu/rsGZBWQ9PjrAU9Htxgbo9MFyR5B/aWoNbBeXn1HLMY1+uMEnXL5pRPlmyVRjgIavWg7UgeNDOw==} + '@ai-sdk/ui-utils@1.0.5': + resolution: {integrity: sha512-DGJSbDf+vJyWmFNexSPUsS1AAy7gtsmFmoSyNbNbJjwl9hRIf2dknfA1V0ahx6pg3NNklNYFm53L8Nphjovfvg==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -1613,8 +1626,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ai@4.0.10: - resolution: {integrity: sha512-40GaEGLbp7if1F50zp3Kr03vcqyGS8svyJWpbkgec7G5Ik2rEtnbDWiUoOJuAVqgP5/iy4NgZQfvX3jRmOyQrw==} + ai@4.0.20: + resolution: {integrity: sha512-dYevYKtREcjSVopBDFWVNca7WJEI1p9Vr9eo7V7fZHzi2vXGDyEa2WYatjFbpR6z6gpVAxKHsof8EoN+B1IAsA==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -2876,6 +2889,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 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: resolution: {integrity: sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==} engines: {node: ^18 || >=20} @@ -3748,24 +3766,37 @@ snapshots: optionalDependencies: 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': dependencies: 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: - '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) - '@ai-sdk/ui-utils': 1.0.2(zod@3.23.8) + json-schema: 0.4.0 + + '@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) throttleit: 2.1.0 optionalDependencies: 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)': dependencies: - '@ai-sdk/provider': 1.0.1 - '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) + '@ai-sdk/provider': 1.0.2 + '@ai-sdk/provider-utils': 2.0.4(zod@3.23.8) zod-to-json-schema: 3.23.5(zod@3.23.8) optionalDependencies: zod: 3.23.8 @@ -4865,12 +4896,12 @@ snapshots: 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: - '@ai-sdk/provider': 1.0.1 - '@ai-sdk/provider-utils': 2.0.2(zod@3.23.8) - '@ai-sdk/react': 1.0.3(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8) - '@ai-sdk/ui-utils': 1.0.2(zod@3.23.8) + '@ai-sdk/provider': 1.0.2 + '@ai-sdk/provider-utils': 2.0.4(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.5(zod@3.23.8) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 zod-to-json-schema: 3.23.5(zod@3.23.8) @@ -6484,6 +6515,8 @@ snapshots: nanoid@3.3.7: {} + nanoid@3.3.8: {} + nanoid@5.0.8: {} natural-compare@1.4.0: {}