From 9778631d6f4e4830c7dfa9401f1a385b84f5457a Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 16 Dec 2024 18:14:40 +0530 Subject: [PATCH] feat: add code and text block types (#609) --- app/(chat)/api/chat/route.ts | 169 +++++++--- app/(chat)/api/document/route.ts | 9 +- app/(chat)/layout.tsx | 15 +- app/globals.css | 258 +++++++------- components/block-actions.tsx | 220 +++++++++--- components/block.tsx | 78 ++++- components/chat.tsx | 1 + components/code-block.tsx | 59 ++++ components/code-editor.tsx | 108 ++++++ components/console.tsx | 126 +++++++ components/document.tsx | 5 +- components/icons.tsx | 152 +++++++++ components/markdown.tsx | 22 +- components/message.tsx | 2 +- components/suggested-actions.tsx | 6 +- components/suggestion.tsx | 11 +- components/toolbar.tsx | 113 +++++-- components/use-block-stream.tsx | 27 +- lib/ai/prompts.ts | 39 ++- lib/db/migrations/0004_odd_slayback.sql | 1 + lib/db/migrations/meta/0004_snapshot.json | 391 ++++++++++++++++++++++ lib/db/migrations/meta/_journal.json | 7 + lib/db/queries.ts | 4 + lib/db/schema.ts | 3 + lib/editor/suggestions.tsx | 10 +- package.json | 7 + pnpm-lock.yaml | 201 +++++++++++ 27 files changed, 1754 insertions(+), 290 deletions(-) create mode 100644 components/code-block.tsx create mode 100644 components/code-editor.tsx create mode 100644 components/console.tsx create mode 100644 lib/db/migrations/0004_odd_slayback.sql create mode 100644 lib/db/migrations/meta/0004_snapshot.json diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 8452f3b..3fa9b37 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2,6 +2,7 @@ import { type Message, StreamData, convertToCoreMessages, + generateObject, streamObject, streamText, } from 'ai'; @@ -10,7 +11,11 @@ import { z } from 'zod'; import { auth } from '@/app/(auth)/auth'; import { customModel } from '@/lib/ai'; import { models } from '@/lib/ai/models'; -import { systemPrompt } from '@/lib/ai/prompts'; +import { + codePrompt, + systemPrompt, + updateDocumentPrompt, +} from '@/lib/ai/prompts'; import { deleteChatById, getChatById, @@ -119,11 +124,12 @@ export async function POST(request: Request) { }, }, createDocument: { - description: 'Create a document for a writing activity', + description: 'Create a document for a writing activity.', parameters: z.object({ title: z.string(), + kind: z.enum(['text', 'code']), }), - execute: async ({ title }) => { + execute: async ({ title, kind }) => { const id = generateUUID(); let draftText = ''; @@ -137,38 +143,75 @@ export async function POST(request: Request) { content: title, }); + streamingData.append({ + type: 'kind', + content: kind, + }); + streamingData.append({ type: 'clear', content: '', }); - const { fullStream } = streamText({ - model: customModel(model.apiIdentifier), - system: - 'Write about the given topic. Markdown is supported. Use headings wherever appropriate.', - prompt: title, - }); + if (kind === 'text') { + const { fullStream } = streamText({ + model: customModel(model.apiIdentifier), + system: + 'Write about the given topic. Markdown is supported. Use headings wherever appropriate.', + prompt: title, + }); - for await (const delta of fullStream) { - const { type } = delta; + for await (const delta of fullStream) { + const { type } = delta; - if (type === 'text-delta') { - const { textDelta } = delta; + if (type === 'text-delta') { + const { textDelta } = delta; - draftText += textDelta; - streamingData.append({ - type: 'text-delta', - content: textDelta, - }); + draftText += textDelta; + streamingData.append({ + type: 'text-delta', + content: textDelta, + }); + } } - } - streamingData.append({ type: 'finish', content: '' }); + streamingData.append({ type: 'finish', content: '' }); + } else if (kind === 'code') { + const { fullStream } = streamObject({ + model: customModel(model.apiIdentifier), + system: codePrompt, + prompt: title, + schema: z.object({ + code: z.string(), + }), + }); + + for await (const delta of fullStream) { + const { type } = delta; + + if (type === 'object') { + const { object } = delta; + const { code } = object; + + if (code) { + streamingData.append({ + type: 'code-delta', + content: code ?? '', + }); + + draftText = code; + } + } + } + + streamingData.append({ type: 'finish', content: '' }); + } if (session.user?.id) { await saveDocument({ id, title, + kind, content: draftText, userId: session.user.id, }); @@ -177,6 +220,7 @@ export async function POST(request: Request) { return { id, title, + kind, content: 'A document was created and is now visible to the user.', }; }, @@ -206,48 +250,73 @@ export async function POST(request: Request) { content: document.title, }); - const { fullStream } = streamText({ - model: customModel(model.apiIdentifier), - system: - 'You are a helpful writing assistant. Based on the description, please update the piece of writing.', - experimental_providerMetadata: { - openai: { - prediction: { - type: 'content', - content: currentContent, + if (document.kind === 'text') { + const { fullStream } = streamText({ + model: customModel(model.apiIdentifier), + system: updateDocumentPrompt(currentContent), + prompt: description, + experimental_providerMetadata: { + openai: { + prediction: { + type: 'content', + content: currentContent, + }, }, }, - }, - messages: [ - { - role: 'user', - content: description, - }, - { role: 'user', content: currentContent }, - ], - }); + }); - for await (const delta of fullStream) { - const { type } = delta; + for await (const delta of fullStream) { + const { type } = delta; - if (type === 'text-delta') { - const { textDelta } = delta; + if (type === 'text-delta') { + const { textDelta } = delta; - draftText += textDelta; - streamingData.append({ - type: 'text-delta', - content: textDelta, - }); + draftText += textDelta; + streamingData.append({ + type: 'text-delta', + content: textDelta, + }); + } } - } - streamingData.append({ type: 'finish', content: '' }); + streamingData.append({ type: 'finish', content: '' }); + } else if (document.kind === 'code') { + const { fullStream } = streamObject({ + model: customModel(model.apiIdentifier), + system: updateDocumentPrompt(currentContent), + prompt: description, + schema: z.object({ + code: z.string(), + }), + }); + + for await (const delta of fullStream) { + const { type } = delta; + + if (type === 'object') { + const { object } = delta; + const { code } = object; + + if (code) { + streamingData.append({ + type: 'code-delta', + content: code ?? '', + }); + + draftText = code; + } + } + } + + streamingData.append({ type: 'finish', content: '' }); + } if (session.user?.id) { await saveDocument({ id, title: document.title, content: draftText, + kind: document.kind, userId: session.user.id, }); } @@ -255,6 +324,7 @@ export async function POST(request: Request) { return { id, title: document.title, + kind: document.kind, content: 'The document has been updated successfully.', }; }, @@ -328,6 +398,7 @@ export async function POST(request: Request) { return { id: documentId, title: document.title, + kind: document.kind, message: 'Suggestions have been added to the document', }; }, diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 9ba2bac..5c84312 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,4 +1,5 @@ import { auth } from '@/app/(auth)/auth'; +import { BlockKind } from '@/components/block'; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, @@ -48,14 +49,18 @@ export async function POST(request: Request) { return new Response('Unauthorized', { status: 401 }); } - const { content, title }: { content: string; title: string } = - await request.json(); + const { + content, + title, + kind, + }: { content: string; title: string; kind: BlockKind } = await request.json(); if (session.user?.id) { const document = await saveDocument({ id, content, title, + kind, userId: session.user.id, }); diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 981aa50..82d5655 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -4,6 +4,7 @@ import { AppSidebar } from '@/components/app-sidebar'; import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { auth } from '../(auth)/auth'; +import Script from 'next/script'; export const experimental_ppr = true; @@ -16,9 +17,15 @@ export default async function Layout({ const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; return ( - - - {children} - + <> +