feat: add image block type (#709)
This commit is contained in:
parent
6c23a8a604
commit
df449a408a
13 changed files with 229 additions and 92 deletions
|
|
@ -2,13 +2,14 @@ import {
|
||||||
type Message,
|
type Message,
|
||||||
convertToCoreMessages,
|
convertToCoreMessages,
|
||||||
createDataStreamResponse,
|
createDataStreamResponse,
|
||||||
|
experimental_generateImage,
|
||||||
streamObject,
|
streamObject,
|
||||||
streamText,
|
streamText,
|
||||||
} from 'ai';
|
} from 'ai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { auth } from '@/app/(auth)/auth';
|
import { auth } from '@/app/(auth)/auth';
|
||||||
import { customModel } from '@/lib/ai';
|
import { customModel, imageGenerationModel } from '@/lib/ai';
|
||||||
import { models } from '@/lib/ai/models';
|
import { models } from '@/lib/ai/models';
|
||||||
import {
|
import {
|
||||||
codePrompt,
|
codePrompt,
|
||||||
|
|
@ -124,10 +125,10 @@ export async function POST(request: Request) {
|
||||||
},
|
},
|
||||||
createDocument: {
|
createDocument: {
|
||||||
description:
|
description:
|
||||||
'Create a document for a writing activity. This tool will call other functions that will generate the contents of the document based on the title and kind.',
|
'Create a document for a writing or content creation activities like image generation. This tool will call other functions that will generate the contents of the document based on the title and kind.',
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
kind: z.enum(['text', 'code']),
|
kind: z.enum(['text', 'code', 'image']),
|
||||||
}),
|
}),
|
||||||
execute: async ({ title, kind }) => {
|
execute: async ({ title, kind }) => {
|
||||||
const id = generateUUID();
|
const id = generateUUID();
|
||||||
|
|
@ -204,6 +205,21 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataStream.writeData({ type: 'finish', content: '' });
|
||||||
|
} else if (kind === 'image') {
|
||||||
|
const { image } = await experimental_generateImage({
|
||||||
|
model: imageGenerationModel,
|
||||||
|
prompt: title,
|
||||||
|
n: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
draftText = image.base64;
|
||||||
|
|
||||||
|
dataStream.writeData({
|
||||||
|
type: 'image-delta',
|
||||||
|
content: image.base64,
|
||||||
|
});
|
||||||
|
|
||||||
dataStream.writeData({ type: 'finish', content: '' });
|
dataStream.writeData({ type: 'finish', content: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -309,6 +325,21 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataStream.writeData({ type: 'finish', content: '' });
|
||||||
|
} else if (document.kind === 'image') {
|
||||||
|
const { image } = await experimental_generateImage({
|
||||||
|
model: imageGenerationModel,
|
||||||
|
prompt: description,
|
||||||
|
n: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
draftText = image.base64;
|
||||||
|
|
||||||
|
dataStream.writeData({
|
||||||
|
type: 'image-delta',
|
||||||
|
content: image.base64,
|
||||||
|
});
|
||||||
|
|
||||||
dataStream.writeData({ type: 'finish', content: '' });
|
dataStream.writeData({ type: 'finish', content: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { cn } from '@/lib/utils';
|
||||||
import { ClockRewind, CopyIcon, RedoIcon, UndoIcon } from './icons';
|
import { ClockRewind, CopyIcon, RedoIcon, UndoIcon } from './icons';
|
||||||
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 { useCopyToClipboard } from 'usehooks-ts';
|
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { ConsoleOutput, UIBlock } from './block';
|
import { ConsoleOutput, UIBlock } from './block';
|
||||||
import { Dispatch, memo, SetStateAction } from 'react';
|
import { Dispatch, memo, SetStateAction } from 'react';
|
||||||
import { RunCodeButton } from './run-code-button';
|
import { RunCodeButton } from './run-code-button';
|
||||||
|
import { useMultimodalCopyToClipboard } from '@/hooks/use-multimodal-copy-to-clipboard';
|
||||||
|
|
||||||
interface BlockActionsProps {
|
interface BlockActionsProps {
|
||||||
block: UIBlock;
|
block: UIBlock;
|
||||||
|
|
@ -25,7 +25,8 @@ function PureBlockActions({
|
||||||
mode,
|
mode,
|
||||||
setConsoleOutputs,
|
setConsoleOutputs,
|
||||||
}: BlockActionsProps) {
|
}: BlockActionsProps) {
|
||||||
const [_, copyToClipboard] = useCopyToClipboard();
|
const { copyTextToClipboard, copyImageToClipboard } =
|
||||||
|
useMultimodalCopyToClipboard();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
|
|
@ -96,7 +97,12 @@ function PureBlockActions({
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="p-2 h-fit dark:hover:bg-zinc-700"
|
className="p-2 h-fit dark:hover:bg-zinc-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
copyToClipboard(block.content);
|
if (block.kind === 'image') {
|
||||||
|
copyImageToClipboard(block.content);
|
||||||
|
} else {
|
||||||
|
copyTextToClipboard(block.content);
|
||||||
|
}
|
||||||
|
|
||||||
toast.success('Copied to clipboard!');
|
toast.success('Copied to clipboard!');
|
||||||
}}
|
}}
|
||||||
disabled={block.status === 'streaming'}
|
disabled={block.status === 'streaming'}
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,9 @@ import { Console } from './console';
|
||||||
import { useSidebar } from './ui/sidebar';
|
import { useSidebar } from './ui/sidebar';
|
||||||
import { useBlock } from '@/hooks/use-block';
|
import { useBlock } from '@/hooks/use-block';
|
||||||
import equal from 'fast-deep-equal';
|
import equal from 'fast-deep-equal';
|
||||||
|
import { ImageEditor } from './image-editor';
|
||||||
|
|
||||||
export type BlockKind = 'text' | 'code';
|
export type BlockKind = 'text' | 'code' | 'image';
|
||||||
|
|
||||||
export interface UIBlock {
|
export interface UIBlock {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -476,7 +477,7 @@ function PureBlock({
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isDocumentsFetching && !block.content ? (
|
{isDocumentsFetching && !block.content ? (
|
||||||
<DocumentSkeleton />
|
<DocumentSkeleton blockKind={block.kind} />
|
||||||
) : block.kind === 'code' ? (
|
) : block.kind === 'code' ? (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
content={
|
content={
|
||||||
|
|
@ -512,9 +513,22 @@ function PureBlock({
|
||||||
newContent={getDocumentContentById(currentVersionIndex)}
|
newContent={getDocumentContentById(currentVersionIndex)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
) : block.kind === 'image' ? (
|
||||||
|
<ImageEditor
|
||||||
|
title={block.title}
|
||||||
|
content={
|
||||||
|
isCurrentVersion
|
||||||
|
? block.content
|
||||||
|
: getDocumentContentById(currentVersionIndex)
|
||||||
|
}
|
||||||
|
isCurrentVersion={isCurrentVersion}
|
||||||
|
currentVersionIndex={currentVersionIndex}
|
||||||
|
status={block.status}
|
||||||
|
isInline={false}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{suggestions ? (
|
{suggestions && suggestions.length > 0 ? (
|
||||||
<div className="md:hidden h-dvh w-12 shrink-0" />
|
<div className="md:hidden h-dvh w-12 shrink-0" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import { BlockKind } from './block';
|
||||||
import { Suggestion } from '@/lib/db/schema';
|
import { Suggestion } from '@/lib/db/schema';
|
||||||
import { initialBlockData, useBlock } from '@/hooks/use-block';
|
import { initialBlockData, useBlock } from '@/hooks/use-block';
|
||||||
import { useUserMessageId } from '@/hooks/use-user-message-id';
|
import { useUserMessageId } from '@/hooks/use-user-message-id';
|
||||||
import { cx } from 'class-variance-authority';
|
|
||||||
import { useSWRConfig } from 'swr';
|
import { useSWRConfig } from 'swr';
|
||||||
|
|
||||||
type DataStreamDelta = {
|
type DataStreamDelta = {
|
||||||
type:
|
type:
|
||||||
| 'text-delta'
|
| 'text-delta'
|
||||||
| 'code-delta'
|
| 'code-delta'
|
||||||
|
| 'image-delta'
|
||||||
| 'title'
|
| 'title'
|
||||||
| 'id'
|
| 'id'
|
||||||
| 'suggestion'
|
| 'suggestion'
|
||||||
|
|
@ -107,6 +107,14 @@ export function DataStreamHandler({ id }: { id: string }) {
|
||||||
status: 'streaming',
|
status: 'streaming',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case 'image-delta':
|
||||||
|
return {
|
||||||
|
...draftBlock,
|
||||||
|
content: delta.content as string,
|
||||||
|
isVisible: true,
|
||||||
|
status: 'streaming',
|
||||||
|
};
|
||||||
|
|
||||||
case 'suggestion':
|
case 'suggestion':
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setOptimisticSuggestions((currentSuggestions) => [
|
setOptimisticSuggestions((currentSuggestions) => [
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ import {
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { UIBlock } from './block';
|
import { BlockKind, UIBlock } from './block';
|
||||||
import { FileIcon, FullscreenIcon, LoaderIcon } from './icons';
|
import { FileIcon, FullscreenIcon, ImageIcon, LoaderIcon } from './icons';
|
||||||
import { cn, fetcher } from '@/lib/utils';
|
import { cn, fetcher } from '@/lib/utils';
|
||||||
import { Document } from '@/lib/db/schema';
|
import { Document } from '@/lib/db/schema';
|
||||||
import { InlineDocumentSkeleton } from './document-skeleton';
|
import { InlineDocumentSkeleton } from './document-skeleton';
|
||||||
|
|
@ -19,6 +19,7 @@ import { DocumentToolCall, DocumentToolResult } from './document';
|
||||||
import { CodeEditor } from './code-editor';
|
import { CodeEditor } from './code-editor';
|
||||||
import { useBlock } from '@/hooks/use-block';
|
import { useBlock } from '@/hooks/use-block';
|
||||||
import equal from 'fast-deep-equal';
|
import equal from 'fast-deep-equal';
|
||||||
|
import { ImageEditor } from './image-editor';
|
||||||
|
|
||||||
interface DocumentPreviewProps {
|
interface DocumentPreviewProps {
|
||||||
isReadonly: boolean;
|
isReadonly: boolean;
|
||||||
|
|
@ -78,7 +79,7 @@ export function DocumentPreview({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDocumentsFetching) {
|
if (isDocumentsFetching) {
|
||||||
return <LoadingSkeleton />;
|
return <LoadingSkeleton blockKind={result.kind ?? args.kind} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const document: Document | null = previewDocument
|
const document: Document | null = previewDocument
|
||||||
|
|
@ -94,13 +95,14 @@ export function DocumentPreview({
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!document) return <LoadingSkeleton />;
|
if (!document) return <LoadingSkeleton blockKind={block.kind} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full cursor-pointer">
|
<div className="relative w-full cursor-pointer">
|
||||||
<HitboxLayer hitboxRef={hitboxRef} result={result} setBlock={setBlock} />
|
<HitboxLayer hitboxRef={hitboxRef} result={result} setBlock={setBlock} />
|
||||||
<DocumentHeader
|
<DocumentHeader
|
||||||
title={document.title}
|
title={document.title}
|
||||||
|
kind={document.kind}
|
||||||
isStreaming={block.status === 'streaming'}
|
isStreaming={block.status === 'streaming'}
|
||||||
/>
|
/>
|
||||||
<DocumentContent document={document} />
|
<DocumentContent document={document} />
|
||||||
|
|
@ -108,7 +110,7 @@ export function DocumentPreview({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingSkeleton = () => (
|
const LoadingSkeleton = ({ blockKind }: { blockKind: BlockKind }) => (
|
||||||
<div className="w-full">
|
<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="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="flex flex-row items-center gap-3">
|
||||||
|
|
@ -121,9 +123,15 @@ const LoadingSkeleton = () => (
|
||||||
<FullscreenIcon />
|
<FullscreenIcon />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{blockKind === 'image' ? (
|
||||||
|
<div className="overflow-y-scroll border rounded-b-2xl bg-muted border-t-0 dark:border-zinc-700">
|
||||||
|
<div className="animate-pulse h-[257px] bg-muted-foreground/20 w-full" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="overflow-y-scroll border rounded-b-2xl p-8 pt-4 bg-muted border-t-0 dark:border-zinc-700">
|
<div className="overflow-y-scroll border rounded-b-2xl p-8 pt-4 bg-muted border-t-0 dark:border-zinc-700">
|
||||||
<InlineDocumentSkeleton />
|
<InlineDocumentSkeleton />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -185,9 +193,11 @@ const HitboxLayer = memo(PureHitboxLayer, (prevProps, nextProps) => {
|
||||||
|
|
||||||
const PureDocumentHeader = ({
|
const PureDocumentHeader = ({
|
||||||
title,
|
title,
|
||||||
|
kind,
|
||||||
isStreaming,
|
isStreaming,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
|
kind: BlockKind;
|
||||||
isStreaming: boolean;
|
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="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">
|
||||||
|
|
@ -197,6 +207,8 @@ const PureDocumentHeader = ({
|
||||||
<div className="animate-spin">
|
<div className="animate-spin">
|
||||||
<LoaderIcon />
|
<LoaderIcon />
|
||||||
</div>
|
</div>
|
||||||
|
) : kind === 'image' ? (
|
||||||
|
<ImageIcon />
|
||||||
) : (
|
) : (
|
||||||
<FileIcon />
|
<FileIcon />
|
||||||
)}
|
)}
|
||||||
|
|
@ -244,6 +256,15 @@ const DocumentContent = ({ document }: { document: Document }) => {
|
||||||
<CodeEditor {...commonProps} />
|
<CodeEditor {...commonProps} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : document.kind === 'image' ? (
|
||||||
|
<ImageEditor
|
||||||
|
title={document.title}
|
||||||
|
content={document.content ?? ''}
|
||||||
|
isCurrentVersion={true}
|
||||||
|
currentVersionIndex={0}
|
||||||
|
status={block.status}
|
||||||
|
isInline={true}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
export const DocumentSkeleton = () => {
|
import { BlockKind } from './block';
|
||||||
return (
|
|
||||||
|
export const DocumentSkeleton = ({ blockKind }: { blockKind: BlockKind }) => {
|
||||||
|
return blockKind === 'image' ? (
|
||||||
|
<div className="flex flex-col gap-4 w-full justify-center items-center h-[calc(100dvh-60px)]">
|
||||||
|
<div className="animate-pulse rounded-lg bg-muted-foreground/20 size-96" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<div className="animate-pulse rounded-lg h-12 bg-muted-foreground/20 w-1/2" />
|
<div className="animate-pulse rounded-lg h-12 bg-muted-foreground/20 w-1/2" />
|
||||||
<div className="animate-pulse rounded-lg h-5 bg-muted-foreground/20 w-full" />
|
<div className="animate-pulse rounded-lg h-5 bg-muted-foreground/20 w-full" />
|
||||||
|
|
|
||||||
50
components/image-editor.tsx
Normal file
50
components/image-editor.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { LoaderIcon } from './icons';
|
||||||
|
import cn from 'classnames';
|
||||||
|
|
||||||
|
interface ImageEditorProps {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
isCurrentVersion: boolean;
|
||||||
|
currentVersionIndex: number;
|
||||||
|
status: string;
|
||||||
|
isInline: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageEditor({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
isCurrentVersion,
|
||||||
|
currentVersionIndex,
|
||||||
|
status,
|
||||||
|
isInline,
|
||||||
|
}: ImageEditorProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn('flex flex-row items-center justify-center w-full', {
|
||||||
|
'h-[calc(100dvh-60px)]': !isInline,
|
||||||
|
'h-[200px]': isInline,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{status === 'streaming' ? (
|
||||||
|
<div className="flex flex-row gap-4 items-center">
|
||||||
|
{!isInline && (
|
||||||
|
<div className="animate-spin">
|
||||||
|
<LoaderIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>Generating Image...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<picture>
|
||||||
|
<img
|
||||||
|
className={cn('w-full h-fit max-w-[800px]', {
|
||||||
|
'p-0 md:p-20': !isInline,
|
||||||
|
})}
|
||||||
|
src={`data:image/png;base64,${content}`}
|
||||||
|
alt={title}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -29,13 +29,12 @@ import { sanitizeUIMessages } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
FileIcon,
|
|
||||||
LogsIcon,
|
LogsIcon,
|
||||||
MessageIcon,
|
MessageIcon,
|
||||||
PenIcon,
|
PenIcon,
|
||||||
|
SparklesIcon,
|
||||||
StopIcon,
|
StopIcon,
|
||||||
SummarizeIcon,
|
SummarizeIcon,
|
||||||
TerminalIcon,
|
|
||||||
} from './icons';
|
} from './icons';
|
||||||
import { BlockKind } from './block';
|
import { BlockKind } from './block';
|
||||||
|
|
||||||
|
|
@ -327,6 +326,7 @@ const toolsByBlockKind: Record<
|
||||||
icon: <LogsIcon />,
|
icon: <LogsIcon />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
image: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Tools = ({
|
export const Tools = ({
|
||||||
|
|
@ -347,7 +347,7 @@ export const Tools = ({
|
||||||
) => Promise<string | null | undefined>;
|
) => Promise<string | null | undefined>;
|
||||||
isAnimating: boolean;
|
isAnimating: boolean;
|
||||||
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
||||||
blockKind: 'text' | 'code';
|
blockKind: BlockKind;
|
||||||
}) => {
|
}) => {
|
||||||
const [primaryTool, ...secondaryTools] = toolsByBlockKind[blockKind];
|
const [primaryTool, ...secondaryTools] = toolsByBlockKind[blockKind];
|
||||||
|
|
||||||
|
|
@ -407,7 +407,7 @@ const PureToolbar = ({
|
||||||
) => Promise<string | null | undefined>;
|
) => Promise<string | null | undefined>;
|
||||||
stop: () => void;
|
stop: () => void;
|
||||||
setMessages: Dispatch<SetStateAction<Message[]>>;
|
setMessages: Dispatch<SetStateAction<Message[]>>;
|
||||||
blockKind: 'text' | 'code';
|
blockKind: BlockKind;
|
||||||
}) => {
|
}) => {
|
||||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
@ -451,6 +451,10 @@ const PureToolbar = ({
|
||||||
}
|
}
|
||||||
}, [isLoading, setIsToolbarVisible]);
|
}, [isLoading, setIsToolbarVisible]);
|
||||||
|
|
||||||
|
if (toolsByBlockKind[blockKind].length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
|
||||||
22
hooks/use-multimodal-copy-to-clipboard.ts
Normal file
22
hooks/use-multimodal-copy-to-clipboard.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { useCopyToClipboard } from 'usehooks-ts';
|
||||||
|
|
||||||
|
async function copyImageToClipboard(base64String: string) {
|
||||||
|
try {
|
||||||
|
const blob = await fetch(`data:image/png;base64,${base64String}`).then(
|
||||||
|
(res) => res.blob(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const item = new ClipboardItem({
|
||||||
|
'image/png': blob,
|
||||||
|
});
|
||||||
|
|
||||||
|
await navigator.clipboard.write([item]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to copy image to clipboard:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMultimodalCopyToClipboard() {
|
||||||
|
const [_, copyTextToClipboard] = useCopyToClipboard();
|
||||||
|
return { copyTextToClipboard, copyImageToClipboard };
|
||||||
|
}
|
||||||
|
|
@ -9,3 +9,5 @@ export const customModel = (apiIdentifier: string) => {
|
||||||
middleware: customMiddleware,
|
middleware: customMiddleware,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const imageGenerationModel = openai.image('dall-e-3');
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ export const document = pgTable(
|
||||||
createdAt: timestamp('createdAt').notNull(),
|
createdAt: timestamp('createdAt').notNull(),
|
||||||
title: text('title').notNull(),
|
title: text('title').notNull(),
|
||||||
content: text('content'),
|
content: text('content'),
|
||||||
kind: varchar('text', { enum: ['text', 'code'] })
|
kind: varchar('text', { enum: ['text', 'code', 'image'] })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default('text'),
|
.default('text'),
|
||||||
userId: uuid('userId')
|
userId: uuid('userId')
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
"db:up": "drizzle-kit up"
|
"db:up": "drizzle-kit up"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "1.0.6",
|
"@ai-sdk/openai": "1.0.19",
|
||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lang-python": "^6.1.6",
|
"@codemirror/lang-python": "^6.1.6",
|
||||||
"@codemirror/state": "^6.5.0",
|
"@codemirror/state": "^6.5.0",
|
||||||
|
|
@ -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.20",
|
"ai": "4.0.36",
|
||||||
"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",
|
||||||
|
|
|
||||||
103
pnpm-lock.yaml
generated
103
pnpm-lock.yaml
generated
|
|
@ -9,8 +9,8 @@ importers:
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/openai':
|
'@ai-sdk/openai':
|
||||||
specifier: 1.0.6
|
specifier: 1.0.19
|
||||||
version: 1.0.6(zod@3.23.8)
|
version: 1.0.19(zod@3.23.8)
|
||||||
'@codemirror/lang-javascript':
|
'@codemirror/lang-javascript':
|
||||||
specifier: ^6.2.2
|
specifier: ^6.2.2
|
||||||
version: 6.2.2
|
version: 6.2.2
|
||||||
|
|
@ -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.20
|
specifier: 4.0.36
|
||||||
version: 4.0.20(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)
|
version: 4.0.36(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
|
||||||
|
|
@ -240,14 +240,14 @@ importers:
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ai-sdk/openai@1.0.6':
|
'@ai-sdk/openai@1.0.19':
|
||||||
resolution: {integrity: sha512-AhNILXn/hVD91mrokg9Wph78BiWNP/N0tJiua+UCS5TYXCbSeeqKqiodRiNxw6tDA5sGnrC6yCv8TjgY1CwzWg==}
|
resolution: {integrity: sha512-7qmLgppWpGUhSgrH0a6CtgD9hZeRh2hARppl1B7fNhVbekYftSMucsdCiVlKbQzSKPxox0vkNMmwjKa/7xf8bQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.0.0
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.0.2':
|
'@ai-sdk/provider-utils@2.0.7':
|
||||||
resolution: {integrity: sha512-IAvhKhdlXqiSmvx/D4uNlFYCl8dWT+M9K+IuEcSgnE2Aj27GWu8sDIpAf4r4Voc+wOUkOECVKQhFo8g9pozdjA==}
|
resolution: {integrity: sha512-4sfPlKEALHPXLmMFcPlYksst3sWBJXmCDZpIBJisRrmwGG6Nn3mq0N1Zu/nZaGcrWZoOY+HT2Wbxla1oTElYHQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.0.0
|
||||||
|
|
@ -255,25 +255,12 @@ packages:
|
||||||
zod:
|
zod:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.0.4':
|
'@ai-sdk/provider@1.0.4':
|
||||||
resolution: {integrity: sha512-GMhcQCZbwM6RoZCri0MWeEWXRt/T+uCxsmHEsTwNvEH3GDjNzchfX25C8ftry2MeEOOn6KfqCLSKomcgK6RoOg==}
|
resolution: {integrity: sha512-lJi5zwDosvvZER3e/pB8lj1MN3o3S7zJliQq56BRr4e9V3fcRyFtwP0JRxaRS5vHYX3OJ154VezVoQNrk0eaKw==}
|
||||||
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'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@ai-sdk/provider@1.0.2':
|
'@ai-sdk/react@1.0.11':
|
||||||
resolution: {integrity: sha512-YYtP6xWQyaAf5LiWLJ+ycGTOeBLWrED7LUrvc+SQIWhGaneylqbaGsyQL7VouQUeQ4JZ1qKYZuhmi3W56HADPA==}
|
resolution: {integrity: sha512-ndBPA7dx2DqUr7s4zO1cRAPkFGS+wWvSri6OWfCuhfyTAADQ4vdd56vFP9zdTZl4cyL27Vh0hKLfFJMGx83MUQ==}
|
||||||
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
|
||||||
|
|
@ -284,8 +271,8 @@ packages:
|
||||||
zod:
|
zod:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@ai-sdk/ui-utils@1.0.5':
|
'@ai-sdk/ui-utils@1.0.10':
|
||||||
resolution: {integrity: sha512-DGJSbDf+vJyWmFNexSPUsS1AAy7gtsmFmoSyNbNbJjwl9hRIf2dknfA1V0ahx6pg3NNklNYFm53L8Nphjovfvg==}
|
resolution: {integrity: sha512-wZfZNH2IloTx5b1O8CU7/R/icm8EsmURElPckYwNYj2YZrKk9X5XeYSDBF/1/J83obzsn0i7VKkIf40qhRzVVA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.0.0
|
zod: ^3.0.0
|
||||||
|
|
@ -1626,8 +1613,8 @@ packages:
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
ai@4.0.20:
|
ai@4.0.36:
|
||||||
resolution: {integrity: sha512-dYevYKtREcjSVopBDFWVNca7WJEI1p9Vr9eo7V7fZHzi2vXGDyEa2WYatjFbpR6z6gpVAxKHsof8EoN+B1IAsA==}
|
resolution: {integrity: sha512-KeXQe+eKQK57WYzN+K6VUBZqj/n70V+cCwrXAhXa7enbZCAsErvQKqrnxkF1qm9UfPuNKNEFV5BRoMKJ5gRfAw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^18 || ^19 || ^19.0.0-rc
|
react: ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
@ -3738,10 +3725,10 @@ packages:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
zod-to-json-schema@3.23.5:
|
zod-to-json-schema@3.24.1:
|
||||||
resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==}
|
resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.23.3
|
zod: ^3.24.1
|
||||||
|
|
||||||
zod@3.23.8:
|
zod@3.23.8:
|
||||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||||
|
|
@ -3751,53 +3738,40 @@ packages:
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ai-sdk/openai@1.0.6(zod@3.23.8)':
|
'@ai-sdk/openai@1.0.19(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.1
|
'@ai-sdk/provider': 1.0.4
|
||||||
'@ai-sdk/provider-utils': 2.0.2(zod@3.23.8)
|
'@ai-sdk/provider-utils': 2.0.7(zod@3.23.8)
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@2.0.2(zod@3.23.8)':
|
'@ai-sdk/provider-utils@2.0.7(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.1
|
'@ai-sdk/provider': 1.0.4
|
||||||
eventsource-parser: 3.0.0
|
|
||||||
nanoid: 3.3.7
|
|
||||||
secure-json-parse: 2.7.0
|
|
||||||
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
|
eventsource-parser: 3.0.0
|
||||||
nanoid: 3.3.8
|
nanoid: 3.3.8
|
||||||
secure-json-parse: 2.7.0
|
secure-json-parse: 2.7.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
'@ai-sdk/provider@1.0.1':
|
'@ai-sdk/provider@1.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema: 0.4.0
|
json-schema: 0.4.0
|
||||||
|
|
||||||
'@ai-sdk/provider@1.0.2':
|
'@ai-sdk/react@1.0.11(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
json-schema: 0.4.0
|
'@ai-sdk/provider-utils': 2.0.7(zod@3.23.8)
|
||||||
|
'@ai-sdk/ui-utils': 1.0.10(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.5(zod@3.23.8)':
|
'@ai-sdk/ui-utils@1.0.10(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.2
|
'@ai-sdk/provider': 1.0.4
|
||||||
'@ai-sdk/provider-utils': 2.0.4(zod@3.23.8)
|
'@ai-sdk/provider-utils': 2.0.7(zod@3.23.8)
|
||||||
zod-to-json-schema: 3.23.5(zod@3.23.8)
|
zod-to-json-schema: 3.24.1(zod@3.23.8)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
|
|
@ -4896,15 +4870,14 @@ snapshots:
|
||||||
|
|
||||||
acorn@8.14.0: {}
|
acorn@8.14.0: {}
|
||||||
|
|
||||||
ai@4.0.20(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8):
|
ai@4.0.36(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ai-sdk/provider': 1.0.2
|
'@ai-sdk/provider': 1.0.4
|
||||||
'@ai-sdk/provider-utils': 2.0.4(zod@3.23.8)
|
'@ai-sdk/provider-utils': 2.0.7(zod@3.23.8)
|
||||||
'@ai-sdk/react': 1.0.6(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)
|
'@ai-sdk/react': 1.0.11(react@19.0.0-rc-45804af1-20241021)(zod@3.23.8)
|
||||||
'@ai-sdk/ui-utils': 1.0.5(zod@3.23.8)
|
'@ai-sdk/ui-utils': 1.0.10(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)
|
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.0.0-rc-45804af1-20241021
|
react: 19.0.0-rc-45804af1-20241021
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
@ -7499,7 +7472,7 @@ snapshots:
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod-to-json-schema@3.23.5(zod@3.23.8):
|
zod-to-json-schema@3.24.1(zod@3.23.8):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.23.8
|
zod: 3.23.8
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue