feat: add code and text block types (#609)
This commit is contained in:
parent
3df0fd4c0f
commit
9778631d6f
27 changed files with 1754 additions and 290 deletions
|
|
@ -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',
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SidebarProvider defaultOpen={!isCollapsed}>
|
||||
<AppSidebar user={session?.user} />
|
||||
<SidebarInset>{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<>
|
||||
<Script
|
||||
src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
|
||||
strategy="beforeInteractive"
|
||||
/>
|
||||
<SidebarProvider defaultOpen={!isCollapsed}>
|
||||
<AppSidebar user={session?.user} />
|
||||
<SidebarInset>{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
258
app/globals.css
258
app/globals.css
|
|
@ -3,144 +3,176 @@
|
|||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-start-rgb: 214, 219, 220;
|
||||
--background-end-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
:root {
|
||||
--foreground-rgb: 255, 255, 255;
|
||||
--background-start-rgb: 0, 0, 0;
|
||||
--background-end-rgb: 0, 0, 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 217 100% 45%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 213 100% 96%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 214 32% 91%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 208.69 100% 24.18%;
|
||||
--primary-foreground: 207 100% 96%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
--primary: 217 100% 45%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 213 100% 96%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 214 32% 91%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 208.69 100% 24.18%;
|
||||
--primary-foreground: 207 100% 96%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "geist";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url(/fonts/geist.woff2) format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "geist";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url(/fonts/geist.woff2) format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "geist-mono";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url(/fonts/geist-mono.woff2) format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "geist-mono";
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
src: url(/fonts/geist-mono.woff2) format("woff2");
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
*[class^="text-"] {
|
||||
color: transparent;
|
||||
@apply rounded-md bg-foreground/20 select-none animate-pulse;
|
||||
}
|
||||
*[class^="text-"] {
|
||||
color: transparent;
|
||||
@apply rounded-md bg-foreground/20 select-none animate-pulse;
|
||||
}
|
||||
|
||||
.skeleton-bg {
|
||||
@apply bg-foreground/10;
|
||||
}
|
||||
.skeleton-bg {
|
||||
@apply bg-foreground/10;
|
||||
}
|
||||
|
||||
.skeleton-div {
|
||||
@apply bg-foreground/20 animate-pulse;
|
||||
}
|
||||
.skeleton-div {
|
||||
@apply bg-foreground/20 animate-pulse;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
outline: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.cm-editor,
|
||||
.cm-gutters {
|
||||
@apply bg-background dark:bg-zinc-800 outline-none selection:bg-zinc-900 !important;
|
||||
}
|
||||
|
||||
.ͼo.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground,
|
||||
.ͼo.cm-selectionBackground,
|
||||
.ͼo.cm-content::selection {
|
||||
@apply bg-zinc-200 dark:bg-zinc-900 !important;
|
||||
}
|
||||
|
||||
.cm-activeLine,
|
||||
.cm-activeLineGutter {
|
||||
@apply bg-transparent !important;
|
||||
}
|
||||
|
||||
.cm-activeLine {
|
||||
@apply rounded-r-sm !important;
|
||||
}
|
||||
|
||||
.cm-lineNumbers {
|
||||
@apply min-w-7;
|
||||
}
|
||||
|
||||
.cm-foldGutter {
|
||||
@apply min-w-3;
|
||||
}
|
||||
|
||||
.cm-lineNumbers .cm-activeLineGutter {
|
||||
@apply rounded-l-sm !important;
|
||||
}
|
||||
|
||||
.suggestion-highlight {
|
||||
@apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40;
|
||||
@apply bg-blue-200 hover:bg-blue-300 dark:hover:bg-blue-400/50 dark:text-blue-50 dark:bg-blue-500/40;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue