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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { cn } from '@/lib/utils';
|
||||
import { CopyIcon, DeltaIcon, RedoIcon, UndoIcon } from './icons';
|
||||
import { cn, generateUUID } from '@/lib/utils';
|
||||
import { ClockRewind, CopyIcon, PlayIcon, RedoIcon, UndoIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { useCopyToClipboard } from 'usehooks-ts';
|
||||
import { toast } from 'sonner';
|
||||
import { UIBlock } from './block';
|
||||
import { memo } from 'react';
|
||||
import { ConsoleOutput, UIBlock } from './block';
|
||||
import {
|
||||
Dispatch,
|
||||
memo,
|
||||
SetStateAction,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
interface BlockActionsProps {
|
||||
block: UIBlock;
|
||||
|
|
@ -13,6 +20,99 @@ interface BlockActionsProps {
|
|||
currentVersionIndex: number;
|
||||
isCurrentVersion: boolean;
|
||||
mode: 'read-only' | 'edit' | 'diff';
|
||||
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
||||
}
|
||||
|
||||
export function RunCodeButton({
|
||||
block,
|
||||
setConsoleOutputs,
|
||||
}: {
|
||||
block: UIBlock;
|
||||
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
||||
}) {
|
||||
const [pyodide, setPyodide] = useState<any>(null);
|
||||
const isPython = true;
|
||||
const codeContent = block.content;
|
||||
|
||||
const updateConsoleOutput = useCallback(
|
||||
(runId: string, content: string | null, status: 'completed' | 'failed') => {
|
||||
setConsoleOutputs((consoleOutputs) => {
|
||||
const index = consoleOutputs.findIndex((output) => output.id === runId);
|
||||
|
||||
if (index === -1) return consoleOutputs;
|
||||
|
||||
const updatedOutputs = [...consoleOutputs];
|
||||
updatedOutputs[index] = {
|
||||
id: runId,
|
||||
content,
|
||||
status,
|
||||
};
|
||||
|
||||
return updatedOutputs;
|
||||
});
|
||||
},
|
||||
[setConsoleOutputs],
|
||||
);
|
||||
|
||||
const loadAndRunPython = useCallback(async () => {
|
||||
const runId = generateUUID();
|
||||
|
||||
setConsoleOutputs((consoleOutputs) => [
|
||||
...consoleOutputs,
|
||||
{
|
||||
id: runId,
|
||||
content: null,
|
||||
status: 'in_progress',
|
||||
},
|
||||
]);
|
||||
|
||||
let currentPyodideInstance = pyodide;
|
||||
|
||||
if (isPython) {
|
||||
if (!currentPyodideInstance) {
|
||||
// @ts-expect-error - pyodide is not defined
|
||||
const newPyodideInstance = await loadPyodide({
|
||||
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
|
||||
});
|
||||
|
||||
setPyodide(newPyodideInstance);
|
||||
currentPyodideInstance = newPyodideInstance;
|
||||
}
|
||||
|
||||
try {
|
||||
await currentPyodideInstance.runPythonAsync(`
|
||||
import sys
|
||||
import io
|
||||
sys.stdout = io.StringIO()
|
||||
`);
|
||||
|
||||
await currentPyodideInstance.runPythonAsync(codeContent);
|
||||
|
||||
const output: string = await currentPyodideInstance.runPythonAsync(
|
||||
`sys.stdout.getvalue()`,
|
||||
);
|
||||
|
||||
updateConsoleOutput(runId, output, 'completed');
|
||||
} catch (error: any) {
|
||||
updateConsoleOutput(runId, error.message, 'failed');
|
||||
}
|
||||
}
|
||||
}, [pyodide, codeContent, isPython, setConsoleOutputs, updateConsoleOutput]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="py-1.5 px-2 h-fit dark:hover:bg-zinc-700"
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
loadAndRunPython();
|
||||
});
|
||||
}}
|
||||
disabled={block.status === 'streaming'}
|
||||
>
|
||||
<PlayIcon size={18} /> Run
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function PureBlockActions({
|
||||
|
|
@ -21,11 +121,73 @@ function PureBlockActions({
|
|||
currentVersionIndex,
|
||||
isCurrentVersion,
|
||||
mode,
|
||||
setConsoleOutputs,
|
||||
}: BlockActionsProps) {
|
||||
const [_, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-1">
|
||||
{block.kind === 'code' && (
|
||||
<RunCodeButton block={block} setConsoleOutputs={setConsoleOutputs} />
|
||||
)}
|
||||
|
||||
{block.kind === 'text' && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'p-2 h-fit !pointer-events-auto dark:hover:bg-zinc-700',
|
||||
{
|
||||
'bg-muted': mode === 'diff',
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
handleVersionChange('toggle');
|
||||
}}
|
||||
disabled={
|
||||
block.status === 'streaming' || currentVersionIndex === 0
|
||||
}
|
||||
>
|
||||
<ClockRewind size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View changes</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
|
||||
onClick={() => {
|
||||
handleVersionChange('prev');
|
||||
}}
|
||||
disabled={currentVersionIndex === 0 || block.status === 'streaming'}
|
||||
>
|
||||
<UndoIcon size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View Previous version</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
|
||||
onClick={() => {
|
||||
handleVersionChange('next');
|
||||
}}
|
||||
disabled={isCurrentVersion || block.status === 'streaming'}
|
||||
>
|
||||
<RedoIcon size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View Next version</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -42,56 +204,6 @@ function PureBlockActions({
|
|||
</TooltipTrigger>
|
||||
<TooltipContent>Copy to clipboard</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
|
||||
onClick={() => {
|
||||
handleVersionChange('prev');
|
||||
}}
|
||||
disabled={currentVersionIndex === 0 || block.status === 'streaming'}
|
||||
>
|
||||
<UndoIcon size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View Previous version</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="p-2 h-fit dark:hover:bg-zinc-700 !pointer-events-auto"
|
||||
onClick={() => {
|
||||
handleVersionChange('next');
|
||||
}}
|
||||
disabled={isCurrentVersion || block.status === 'streaming'}
|
||||
>
|
||||
<RedoIcon size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View Next version</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'p-2 h-fit !pointer-events-auto dark:hover:bg-zinc-700',
|
||||
{
|
||||
'bg-muted': mode === 'diff',
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
handleVersionChange('toggle');
|
||||
}}
|
||||
disabled={block.status === 'streaming' || currentVersionIndex === 0}
|
||||
>
|
||||
<DeltaIcon size={18} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>View changes</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import useSWR, { useSWRConfig } from 'swr';
|
|||
import { useDebounceCallback, useWindowSize } from 'usehooks-ts';
|
||||
|
||||
import type { Document, Suggestion, Vote } from '@/lib/db/schema';
|
||||
import { fetcher } from '@/lib/utils';
|
||||
import { cn, fetcher } from '@/lib/utils';
|
||||
|
||||
import { DiffView } from './diffview';
|
||||
import { DocumentSkeleton } from './document-skeleton';
|
||||
|
|
@ -29,10 +29,15 @@ import { VersionFooter } from './version-footer';
|
|||
import { BlockActions } from './block-actions';
|
||||
import { BlockCloseButton } from './block-close-button';
|
||||
import { BlockMessages } from './block-messages';
|
||||
import { CodeEditor } from './code-editor';
|
||||
import { Console } from './console';
|
||||
|
||||
export type BlockKind = 'text' | 'code';
|
||||
|
||||
export interface UIBlock {
|
||||
title: string;
|
||||
documentId: string;
|
||||
kind: BlockKind;
|
||||
content: string;
|
||||
isVisible: boolean;
|
||||
status: 'streaming' | 'idle';
|
||||
|
|
@ -44,6 +49,12 @@ export interface UIBlock {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ConsoleOutput {
|
||||
id: string;
|
||||
status: 'in_progress' | 'completed' | 'failed';
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
function PureBlock({
|
||||
chatId,
|
||||
input,
|
||||
|
|
@ -113,6 +124,9 @@ function PureBlock({
|
|||
const [mode, setMode] = useState<'edit' | 'diff'>('edit');
|
||||
const [document, setDocument] = useState<Document | null>(null);
|
||||
const [currentVersionIndex, setCurrentVersionIndex] = useState(-1);
|
||||
const [consoleOutputs, setConsoleOutputs] = useState<Array<ConsoleOutput>>(
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (documents && documents.length > 0) {
|
||||
|
|
@ -158,6 +172,7 @@ function PureBlock({
|
|||
body: JSON.stringify({
|
||||
title: block.title,
|
||||
content: updatedContent,
|
||||
kind: block.kind,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -318,7 +333,7 @@ function PureBlock({
|
|||
)}
|
||||
|
||||
<motion.div
|
||||
className="fixed dark:bg-muted bg-background h-dvh flex flex-col shadow-xl overflow-y-scroll"
|
||||
className="fixed dark:bg-muted bg-background h-dvh flex flex-col overflow-y-scroll"
|
||||
initial={
|
||||
isMobile
|
||||
? {
|
||||
|
|
@ -415,15 +430,29 @@ function PureBlock({
|
|||
handleVersionChange={handleVersionChange}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
mode={mode}
|
||||
setConsoleOutputs={setConsoleOutputs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll px-4 py-8 md:p-20 !max-w-full pb-40 items-center">
|
||||
<div className="flex flex-row max-w-[600px] mx-auto">
|
||||
<div
|
||||
className={cn(
|
||||
'prose dark:prose-invert dark:bg-muted bg-background h-full overflow-y-scroll !max-w-full pb-40 items-center',
|
||||
{
|
||||
'py-2 px-2': block.kind === 'code',
|
||||
'py-8 md:p-20 px-4': block.kind === 'text',
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn('flex flex-row', {
|
||||
'': block.kind === 'code',
|
||||
'mx-auto max-w-[600px]': block.kind === 'text',
|
||||
})}
|
||||
>
|
||||
{isDocumentsFetching && !block.content ? (
|
||||
<DocumentSkeleton />
|
||||
) : mode === 'edit' ? (
|
||||
<Editor
|
||||
) : block.kind === 'code' ? (
|
||||
<CodeEditor
|
||||
content={
|
||||
isCurrentVersion
|
||||
? block.content
|
||||
|
|
@ -431,16 +460,31 @@ function PureBlock({
|
|||
}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
suggestions={suggestions ?? []}
|
||||
status={block.status}
|
||||
saveContent={saveContent}
|
||||
suggestions={isCurrentVersion ? (suggestions ?? []) : []}
|
||||
/>
|
||||
) : (
|
||||
<DiffView
|
||||
oldContent={getDocumentContentById(currentVersionIndex - 1)}
|
||||
newContent={getDocumentContentById(currentVersionIndex)}
|
||||
/>
|
||||
)}
|
||||
) : block.kind === 'text' ? (
|
||||
mode === 'edit' ? (
|
||||
<Editor
|
||||
content={
|
||||
isCurrentVersion
|
||||
? block.content
|
||||
: getDocumentContentById(currentVersionIndex)
|
||||
}
|
||||
isCurrentVersion={isCurrentVersion}
|
||||
currentVersionIndex={currentVersionIndex}
|
||||
status={block.status}
|
||||
saveContent={saveContent}
|
||||
suggestions={isCurrentVersion ? (suggestions ?? []) : []}
|
||||
/>
|
||||
) : (
|
||||
<DiffView
|
||||
oldContent={getDocumentContentById(currentVersionIndex - 1)}
|
||||
newContent={getDocumentContentById(currentVersionIndex)}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{suggestions ? (
|
||||
<div className="md:hidden h-dvh w-12 shrink-0" />
|
||||
|
|
@ -455,6 +499,7 @@ function PureBlock({
|
|||
isLoading={isLoading}
|
||||
stop={stop}
|
||||
setMessages={setMessages}
|
||||
blockKind={block.kind}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
@ -471,6 +516,13 @@ function PureBlock({
|
|||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
<Console
|
||||
consoleOutputs={consoleOutputs}
|
||||
setConsoleOutputs={setConsoleOutputs}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export function Chat({
|
|||
const [block, setBlock] = useState<UIBlock>({
|
||||
documentId: 'init',
|
||||
content: '',
|
||||
kind: 'text',
|
||||
title: '',
|
||||
status: 'idle',
|
||||
isVisible: false,
|
||||
|
|
|
|||
59
components/code-block.tsx
Normal file
59
components/code-block.tsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback, useState } from 'react';
|
||||
import { CodeIcon, LoaderIcon, PlayIcon, PythonIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface CodeBlockProps {
|
||||
node: any;
|
||||
inline: boolean;
|
||||
className: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export function CodeBlock({
|
||||
node,
|
||||
inline,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: CodeBlockProps) {
|
||||
const [output, setOutput] = useState<string | null>(null);
|
||||
const [pyodide, setPyodide] = useState<any>(null);
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isPython = match && match[1] === 'python';
|
||||
const codeContent = String(children).replace(/\n$/, '');
|
||||
const [tab, setTab] = useState<'code' | 'run'>('code');
|
||||
|
||||
if (!inline) {
|
||||
return (
|
||||
<div className="not-prose flex flex-col">
|
||||
{tab === 'code' && (
|
||||
<pre
|
||||
{...props}
|
||||
className={`text-sm w-full overflow-x-auto dark:bg-zinc-900 p-4 border border-zinc-200 dark:border-zinc-700 rounded-xl dark:text-zinc-50 text-zinc-900`}
|
||||
>
|
||||
<code className="whitespace-pre-wrap break-words">{children}</code>
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{tab === 'run' && output && (
|
||||
<div className="text-sm w-full overflow-x-auto bg-zinc-800 dark:bg-zinc-900 p-4 border border-zinc-200 dark:border-zinc-700 border-t-0 rounded-b-xl text-zinc-50">
|
||||
<code>{output}</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<code
|
||||
className={`${className} text-sm bg-zinc-100 dark:bg-zinc-800 py-0.5 px-1 rounded-md`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
}
|
||||
108
components/code-editor.tsx
Normal file
108
components/code-editor.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
'use client';
|
||||
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { EditorState, Transaction } from '@codemirror/state';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import { Suggestion } from '@/lib/db/schema';
|
||||
|
||||
type EditorProps = {
|
||||
content: string;
|
||||
saveContent: (updatedContent: string, debounce: boolean) => void;
|
||||
status: 'streaming' | 'idle';
|
||||
isCurrentVersion: boolean;
|
||||
currentVersionIndex: number;
|
||||
suggestions: Array<Suggestion>;
|
||||
};
|
||||
|
||||
function PureCodeEditor({ content, saveContent, status }: EditorProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current && !editorRef.current) {
|
||||
const startState = EditorState.create({
|
||||
doc: content,
|
||||
extensions: [basicSetup, python(), oneDark],
|
||||
});
|
||||
|
||||
editorRef.current = new EditorView({
|
||||
state: startState,
|
||||
parent: containerRef.current,
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.destroy();
|
||||
editorRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
const transaction = update.transactions.find(
|
||||
(tr) => !tr.annotation(Transaction.remote),
|
||||
);
|
||||
|
||||
if (transaction) {
|
||||
const newContent = update.state.doc.toString();
|
||||
saveContent(newContent, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const newState = EditorState.create({
|
||||
doc: editorRef.current.state.doc,
|
||||
extensions: [basicSetup, python(), oneDark, updateListener],
|
||||
});
|
||||
|
||||
editorRef.current.setState(newState);
|
||||
}
|
||||
}, [saveContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current && content) {
|
||||
const currentContent = editorRef.current.state.doc.toString();
|
||||
|
||||
if (status === 'streaming' || currentContent !== content) {
|
||||
const transaction = editorRef.current.state.update({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: currentContent.length,
|
||||
insert: content,
|
||||
},
|
||||
annotations: [Transaction.remote.of(true)],
|
||||
});
|
||||
|
||||
editorRef.current.dispatch(transaction);
|
||||
}
|
||||
}
|
||||
}, [content, status]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative not-prose w-full pb-[calc(80dvh)] text-sm"
|
||||
ref={containerRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function areEqual(prevProps: EditorProps, nextProps: EditorProps) {
|
||||
if (prevProps.suggestions !== nextProps.suggestions) return false;
|
||||
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex)
|
||||
return false;
|
||||
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false;
|
||||
if (prevProps.status === 'streaming' && nextProps.status === 'streaming')
|
||||
return false;
|
||||
if (prevProps.content !== nextProps.content) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export const CodeEditor = memo(PureCodeEditor, areEqual);
|
||||
126
components/console.tsx
Normal file
126
components/console.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { TerminalWindowIcon, LoaderIcon, CrossSmallIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ConsoleOutput } from './block';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ConsoleProps {
|
||||
consoleOutputs: Array<ConsoleOutput>;
|
||||
setConsoleOutputs: Dispatch<SetStateAction<Array<ConsoleOutput>>>;
|
||||
}
|
||||
|
||||
export function Console({ consoleOutputs, setConsoleOutputs }: ConsoleProps) {
|
||||
const [height, setHeight] = useState<number>(300);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const consoleEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const minHeight = 100;
|
||||
const maxHeight = 800;
|
||||
|
||||
const startResizing = useCallback(() => {
|
||||
setIsResizing(true);
|
||||
}, []);
|
||||
|
||||
const stopResizing = useCallback(() => {
|
||||
setIsResizing(false);
|
||||
}, []);
|
||||
|
||||
const resize = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (isResizing) {
|
||||
const newHeight = window.innerHeight - e.clientY;
|
||||
if (newHeight >= minHeight && newHeight <= maxHeight) {
|
||||
setHeight(newHeight);
|
||||
}
|
||||
}
|
||||
},
|
||||
[isResizing],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mousemove', resize);
|
||||
window.addEventListener('mouseup', stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', resize);
|
||||
window.removeEventListener('mouseup', stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
|
||||
useEffect(() => {
|
||||
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [consoleOutputs]);
|
||||
|
||||
return consoleOutputs.length > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className="h-2 w-full fixed cursor-ns-resize z-50"
|
||||
onMouseDown={startResizing}
|
||||
style={{ bottom: height - 4 }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'fixed flex flex-col bottom-0 dark:bg-zinc-900 bg-zinc-50 w-full border-t z-40 overflow-y-scroll dark:border-zinc-700 border-zinc-200',
|
||||
{
|
||||
'select-none': isResizing,
|
||||
},
|
||||
)}
|
||||
style={{ height }}
|
||||
>
|
||||
<div className="flex flex-row justify-between items-center w-full h-fit border-b dark:border-zinc-700 border-zinc-200 px-2 py-1 sticky top-0 z-50 bg-muted">
|
||||
<div className="text-sm pl-2 dark:text-zinc-50 text-zinc-800 flex flex-row gap-3 items-center">
|
||||
<div className="text-muted-foreground">
|
||||
<TerminalWindowIcon />
|
||||
</div>
|
||||
<div>Console</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="size-fit p-1 hover:dark:bg-zinc-700 hover:bg-zinc-200"
|
||||
size="icon"
|
||||
onClick={() => setConsoleOutputs([])}
|
||||
>
|
||||
<CrossSmallIcon />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{consoleOutputs.map((consoleOutput, index) => (
|
||||
<div
|
||||
key={consoleOutput.id}
|
||||
className="px-4 py-2 flex flex-row text-sm border-b dark:border-zinc-700 border-zinc-200 dark:bg-zinc-900 bg-zinc-50 font-mono"
|
||||
>
|
||||
<div
|
||||
className={cn('w-12 shrink-0', {
|
||||
'text-muted-foreground':
|
||||
consoleOutput.status === 'in_progress',
|
||||
'text-emerald-500': consoleOutput.status === 'completed',
|
||||
'text-red-400': consoleOutput.status === 'failed',
|
||||
})}
|
||||
>
|
||||
[{index + 1}]
|
||||
</div>
|
||||
{consoleOutput.status === 'in_progress' ? (
|
||||
<div className="animate-spin size-fit self-center">
|
||||
<LoaderIcon />
|
||||
</div>
|
||||
) : (
|
||||
<div className="dark:text-zinc-50 text-zinc-900 whitespace-pre-line">
|
||||
{consoleOutput.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={consoleEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { memo, type SetStateAction } from 'react';
|
||||
|
||||
import type { UIBlock } from './block';
|
||||
import type { BlockKind, UIBlock } from './block';
|
||||
import { FileIcon, LoaderIcon, MessageIcon, PencilEditIcon } from './icons';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ const getActionText = (
|
|||
|
||||
interface DocumentToolResultProps {
|
||||
type: 'create' | 'update' | 'request-suggestions';
|
||||
result: { id: string; title: string };
|
||||
result: { id: string; title: string; kind: BlockKind };
|
||||
block: UIBlock;
|
||||
setBlock: (value: SetStateAction<UIBlock>) => void;
|
||||
isReadonly: boolean;
|
||||
|
|
@ -59,6 +59,7 @@ function PureDocumentToolResult({
|
|||
|
||||
setBlock({
|
||||
documentId: result.id,
|
||||
kind: result.kind,
|
||||
content: '',
|
||||
title: result.title,
|
||||
isVisible: true,
|
||||
|
|
|
|||
|
|
@ -627,6 +627,23 @@ export const CrossIcon = ({ size = 16 }: { size?: number }) => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
export const CrossSmallIcon = ({ size = 16 }: { size?: number }) => (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.96966 11.0303L10.5 11.5607L11.5607 10.5L11.0303 9.96966L9.06065 7.99999L11.0303 6.03032L11.5607 5.49999L10.5 4.43933L9.96966 4.96966L7.99999 6.93933L6.03032 4.96966L5.49999 4.43933L4.43933 5.49999L4.96966 6.03032L6.93933 7.99999L4.96966 9.96966L4.43933 10.5L5.49999 11.5607L6.03032 11.0303L7.99999 9.06065L9.96966 11.0303Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const UndoIcon = ({ size = 16 }: { size?: number }) => (
|
||||
<svg
|
||||
height={size}
|
||||
|
|
@ -931,3 +948,138 @@ export const ShareIcon = ({ size = 16 }: { size?: number }) => {
|
|||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.21969 12.5303L4.75002 13.0607L5.81068 12L5.28035 11.4697L1.81068 7.99999L5.28035 4.53032L5.81068 3.99999L4.75002 2.93933L4.21969 3.46966L0.39647 7.29289C0.00594562 7.68341 0.00594562 8.31658 0.39647 8.7071L4.21969 12.5303ZM11.7804 12.5303L11.25 13.0607L10.1894 12L10.7197 11.4697L14.1894 7.99999L10.7197 4.53032L10.1894 3.99999L11.25 2.93933L11.7804 3.46966L15.6036 7.29289C15.9941 7.68341 15.9941 8.31658 15.6036 8.7071L11.7804 12.5303Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlayIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M13.4549 7.22745L13.3229 7.16146L2.5 1.74999L2.4583 1.72914L1.80902 1.4045L1.3618 1.18089C1.19558 1.09778 1 1.21865 1 1.4045L1 1.9045L1 2.63041L1 2.67704L1 13.3229L1 13.3696L1 14.0955L1 14.5955C1 14.7813 1.19558 14.9022 1.3618 14.8191L1.80902 14.5955L2.4583 14.2708L2.5 14.25L13.3229 8.83852L13.4549 8.77253L14.2546 8.37267L14.5528 8.2236C14.737 8.13147 14.737 7.86851 14.5528 7.77638L14.2546 7.62731L13.4549 7.22745ZM11.6459 7.99999L2.5 3.42704L2.5 12.5729L11.6459 7.99999Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const PythonIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
d="M7.90474 0.00013087C7.24499 0.00316291 6.61494 0.0588153 6.06057 0.15584C4.42745 0.441207 4.13094 1.0385 4.13094 2.14002V3.59479H7.9902V4.07971H4.13094H2.68259C1.56099 4.07971 0.578874 4.7465 0.271682 6.01496C-0.0826597 7.4689 -0.0983767 8.37619 0.271682 9.89434C0.546012 11.0244 1.20115 11.8296 2.32276 11.8296H3.64966V10.0856C3.64966 8.82574 4.75179 7.71441 6.06057 7.71441H9.91533C10.9884 7.71441 11.845 6.84056 11.845 5.77472V2.14002C11.845 1.10556 10.9626 0.328487 9.91533 0.15584C9.25237 0.046687 8.56448 -0.00290121 7.90474 0.00013087ZM5.81768 1.17017C6.21631 1.17017 6.54185 1.49742 6.54185 1.89978C6.54185 2.30072 6.21631 2.62494 5.81768 2.62494C5.41761 2.62494 5.09351 2.30072 5.09351 1.89978C5.09351 1.49742 5.41761 1.17017 5.81768 1.17017Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
<path
|
||||
d="M12.3262 4.07971V5.77472C12.3262 7.08883 11.1997 8.19488 9.91525 8.19488H6.06049C5.0046 8.19488 4.13086 9.0887 4.13086 10.1346V13.7693C4.13086 14.8037 5.04033 15.4122 6.06049 15.709C7.28211 16.0642 8.45359 16.1285 9.91525 15.709C10.8868 15.4307 11.8449 14.8708 11.8449 13.7693V12.3145H7.99012V11.8296H11.8449H13.7745C14.8961 11.8296 15.3141 11.0558 15.7041 9.89434C16.1071 8.69865 16.0899 7.5488 15.7041 6.01495C15.4269 4.91058 14.8975 4.07971 13.7745 4.07971H12.3262ZM10.1581 13.2843C10.5582 13.2843 10.8823 13.6086 10.8823 14.0095C10.8823 14.4119 10.5582 14.7391 10.1581 14.7391C9.7595 14.7391 9.43397 14.4119 9.43397 14.0095C9.43397 13.6086 9.7595 13.2843 10.1581 13.2843Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TerminalWindowIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.5 2.5H14.5V12.5C14.5 13.0523 14.0523 13.5 13.5 13.5H2.5C1.94772 13.5 1.5 13.0523 1.5 12.5V2.5ZM0 1H1.5H14.5H16V2.5V12.5C16 13.8807 14.8807 15 13.5 15H2.5C1.11929 15 0 13.8807 0 12.5V2.5V1ZM4 11.1339L4.44194 10.6919L6.51516 8.61872C6.85687 8.27701 6.85687 7.72299 6.51517 7.38128L4.44194 5.30806L4 4.86612L3.11612 5.75L3.55806 6.19194L5.36612 8L3.55806 9.80806L3.11612 10.25L4 11.1339ZM8 9.75494H8.6225H11.75H12.3725V10.9999H11.75H8.6225H8V9.75494Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const TerminalIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.53035 12.7804L1.00002 13.3108L-0.0606384 12.2501L0.469692 11.7198L4.18936 8.00011L0.469692 4.28044L-0.0606384 3.75011L1.00002 2.68945L1.53035 3.21978L5.60358 7.29301C5.9941 7.68353 5.9941 8.3167 5.60357 8.70722L1.53035 12.7804ZM8.75002 12.5001H8.00002V14.0001H8.75002H15.25H16V12.5001H15.25H8.75002Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClockRewind = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.96452 2.5C11.0257 2.5 13.5 4.96643 13.5 8C13.5 11.0336 11.0257 13.5 7.96452 13.5C6.12055 13.5 4.48831 12.6051 3.48161 11.2273L3.03915 10.6217L1.828 11.5066L2.27046 12.1122C3.54872 13.8617 5.62368 15 7.96452 15C11.8461 15 15 11.87 15 8C15 4.13001 11.8461 1 7.96452 1C5.06835 1 2.57851 2.74164 1.5 5.23347V3.75V3H0V3.75V7.25C0 7.66421 0.335786 8 0.75 8H3.75H4.5V6.5H3.75H2.63724C3.29365 4.19393 5.42843 2.5 7.96452 2.5ZM8.75 5.25V4.5H7.25V5.25V7.8662C7.25 8.20056 7.4171 8.51279 7.6953 8.69825L9.08397 9.62404L9.70801 10.0401L10.5401 8.79199L9.91603 8.37596L8.75 7.59861V5.25Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsIcon = ({ size = 16 }: { size?: number }) => {
|
||||
return (
|
||||
<svg
|
||||
height={size}
|
||||
strokeLinejoin="round"
|
||||
viewBox="0 0 16 16"
|
||||
width={size}
|
||||
style={{ color: 'currentcolor' }}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 2H9.75H14.25H15V3.5H14.25H9.75H9V2ZM9 12.5H9.75H14.25H15V14H14.25H9.75H9V12.5ZM9.75 7.25H9V8.75H9.75H14.25H15V7.25H14.25H9.75ZM1 12.5H1.75H2.25H3V14H2.25H1.75H1V12.5ZM1.75 2H1V3.5H1.75H2.25H3V2H2.25H1.75ZM1 7.25H1.75H2.25H3V8.75H2.25H1.75H1V7.25ZM5.75 12.5H5V14H5.75H6.25H7V12.5H6.25H5.75ZM5 2H5.75H6.25H7V3.5H6.25H5.75H5V2ZM5.75 7.25H5V8.75H5.75H6.25H7V7.25H6.25H5.75Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,29 +2,13 @@ import Link from 'next/link';
|
|||
import React, { memo } from 'react';
|
||||
import ReactMarkdown, { type Components } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { CodeBlock } from './code-block';
|
||||
|
||||
const NonMemoizedMarkdown = ({ children }: { children: string }) => {
|
||||
const components: Partial<Components> = {
|
||||
// @ts-expect-error
|
||||
code: ({ node, inline, className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
// @ts-expect-error
|
||||
<pre
|
||||
{...props}
|
||||
className={`${className} text-sm w-[80dvw] md:max-w-[500px] overflow-x-scroll bg-zinc-100 p-3 rounded-lg mt-2 dark:bg-zinc-800`}
|
||||
>
|
||||
<code className={match[1]}>{children}</code>
|
||||
</pre>
|
||||
) : (
|
||||
<code
|
||||
className={`${className} text-sm bg-zinc-100 dark:bg-zinc-800 py-0.5 px-1 rounded-md`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
code: CodeBlock,
|
||||
pre: ({ children }) => <>{children}</>,
|
||||
ol: ({ node, children, ...props }) => {
|
||||
return (
|
||||
<ol className="list-decimal list-outside ml-4" {...props}>
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ const PurePreviewMessage = ({
|
|||
)}
|
||||
>
|
||||
{message.role === 'assistant' && (
|
||||
<div className="size-8 flex items-center rounded-full justify-center ring-1 shrink-0 ring-border">
|
||||
<div className="size-8 flex items-center rounded-full justify-center ring-1 shrink-0 ring-border bg-background">
|
||||
<SparklesIcon size={14} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ function PureSuggestedActions({ chatId, append }: SuggestedActionsProps) {
|
|||
action: 'What is the weather in San Francisco?',
|
||||
},
|
||||
{
|
||||
title: 'Help me draft an essay',
|
||||
label: 'about Silicon Valley',
|
||||
action: 'Help me draft a short essay about Silicon Valley',
|
||||
title: 'Help me write code to',
|
||||
label: 'demonstrate the djikstra algorithm!',
|
||||
action: 'Help me write code to demonstrate the djikstra algorithm!',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -8,13 +8,17 @@ import type { UISuggestion } from '@/lib/editor/suggestions';
|
|||
|
||||
import { CrossIcon, MessageIcon } from './icons';
|
||||
import { Button } from './ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BlockKind } from './block';
|
||||
|
||||
export const Suggestion = ({
|
||||
suggestion,
|
||||
onApply,
|
||||
blockKind,
|
||||
}: {
|
||||
suggestion: UISuggestion;
|
||||
onApply: () => void;
|
||||
blockKind: BlockKind;
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
|
@ -23,7 +27,10 @@ export const Suggestion = ({
|
|||
<AnimatePresence>
|
||||
{!isExpanded ? (
|
||||
<motion.div
|
||||
className="absolute cursor-pointer text-muted-foreground -right-8 p-1"
|
||||
className={cn('cursor-pointer text-muted-foreground p-1', {
|
||||
'absolute -right-8': blockKind === 'text',
|
||||
'sticky top-0 right-4': blockKind === 'code',
|
||||
})}
|
||||
onClick={() => {
|
||||
setIsExpanded(true);
|
||||
}}
|
||||
|
|
@ -34,7 +41,7 @@ export const Suggestion = ({
|
|||
) : (
|
||||
<motion.div
|
||||
key={suggestion.id}
|
||||
className="absolute bg-background p-3 flex flex-col gap-3 rounded-2xl border text-sm w-56 shadow-xl z-50 -right-12 md:-right-16"
|
||||
className="absolute bg-background p-3 flex flex-col gap-3 rounded-2xl border text-sm w-56 shadow-xl z-50 -right-12 md:-right-16 font-sans"
|
||||
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: -20 }}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,25 @@ import { sanitizeUIMessages } from '@/lib/utils';
|
|||
|
||||
import {
|
||||
ArrowUpIcon,
|
||||
CodeIcon,
|
||||
FileIcon,
|
||||
LogsIcon,
|
||||
MessageIcon,
|
||||
PenIcon,
|
||||
StopIcon,
|
||||
SummarizeIcon,
|
||||
TerminalIcon,
|
||||
} from './icons';
|
||||
import equal from 'fast-deep-equal';
|
||||
import { BlockKind } from './block';
|
||||
|
||||
type ToolProps = {
|
||||
type: 'final-polish' | 'request-suggestions' | 'adjust-reading-level';
|
||||
type:
|
||||
| 'final-polish'
|
||||
| 'request-suggestions'
|
||||
| 'adjust-reading-level'
|
||||
| 'code-review'
|
||||
| 'add-comments'
|
||||
| 'add-logs';
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
selectedTool: string | null;
|
||||
|
|
@ -99,6 +109,20 @@ const Tool = ({
|
|||
'Please add suggestions you have that could improve the writing.',
|
||||
});
|
||||
|
||||
setSelectedTool(null);
|
||||
} else if (type === 'add-comments') {
|
||||
append({
|
||||
role: 'user',
|
||||
content: 'Please add comments to explain the code.',
|
||||
});
|
||||
|
||||
setSelectedTool(null);
|
||||
} else if (type === 'add-logs') {
|
||||
append({
|
||||
role: 'user',
|
||||
content: 'Please add logs to help debug the code.',
|
||||
});
|
||||
|
||||
setSelectedTool(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -260,6 +284,51 @@ const ReadingLevelSelector = ({
|
|||
);
|
||||
};
|
||||
|
||||
const toolsByBlockKind: Record<
|
||||
BlockKind,
|
||||
Array<{
|
||||
type:
|
||||
| 'final-polish'
|
||||
| 'request-suggestions'
|
||||
| 'adjust-reading-level'
|
||||
| 'code-review'
|
||||
| 'add-comments'
|
||||
| 'add-logs';
|
||||
description: string;
|
||||
icon: JSX.Element;
|
||||
}>
|
||||
> = {
|
||||
text: [
|
||||
{
|
||||
type: 'final-polish',
|
||||
description: 'Add final polish',
|
||||
icon: <PenIcon />,
|
||||
},
|
||||
{
|
||||
type: 'adjust-reading-level',
|
||||
description: 'Adjust reading level',
|
||||
icon: <SummarizeIcon />,
|
||||
},
|
||||
{
|
||||
type: 'request-suggestions',
|
||||
description: 'Request suggestions',
|
||||
icon: <MessageIcon />,
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{
|
||||
type: 'add-comments',
|
||||
description: 'Add comments',
|
||||
icon: <CodeIcon />,
|
||||
},
|
||||
{
|
||||
type: 'add-logs',
|
||||
description: 'Add logs',
|
||||
icon: <LogsIcon />,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const Tools = ({
|
||||
isToolbarVisible,
|
||||
selectedTool,
|
||||
|
|
@ -267,6 +336,7 @@ export const Tools = ({
|
|||
append,
|
||||
isAnimating,
|
||||
setIsToolbarVisible,
|
||||
blockKind,
|
||||
}: {
|
||||
isToolbarVisible: boolean;
|
||||
selectedTool: string | null;
|
||||
|
|
@ -277,7 +347,10 @@ export const Tools = ({
|
|||
) => Promise<string | null | undefined>;
|
||||
isAnimating: boolean;
|
||||
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
||||
blockKind: 'text' | 'code';
|
||||
}) => {
|
||||
const [primaryTool, ...secondaryTools] = toolsByBlockKind[blockKind];
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className="flex flex-col"
|
||||
|
|
@ -286,35 +359,25 @@ export const Tools = ({
|
|||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{isToolbarVisible && (
|
||||
<>
|
||||
{isToolbarVisible &&
|
||||
secondaryTools.map((secondaryTool) => (
|
||||
<Tool
|
||||
type="adjust-reading-level"
|
||||
description="Adjust reading level"
|
||||
icon={<SummarizeIcon />}
|
||||
key={secondaryTool.type}
|
||||
type={secondaryTool.type}
|
||||
description={secondaryTool.description}
|
||||
icon={secondaryTool.icon}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={setSelectedTool}
|
||||
append={append}
|
||||
isAnimating={isAnimating}
|
||||
/>
|
||||
|
||||
<Tool
|
||||
type="request-suggestions"
|
||||
description="Request suggestions"
|
||||
icon={<MessageIcon />}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={setSelectedTool}
|
||||
append={append}
|
||||
isAnimating={isAnimating}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
<Tool
|
||||
type="final-polish"
|
||||
description="Add final polish"
|
||||
icon={<PenIcon />}
|
||||
type={primaryTool.type}
|
||||
description={primaryTool.description}
|
||||
icon={primaryTool.icon}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={setSelectedTool}
|
||||
isToolbarVisible={isToolbarVisible}
|
||||
|
|
@ -333,6 +396,7 @@ const PureToolbar = ({
|
|||
isLoading,
|
||||
stop,
|
||||
setMessages,
|
||||
blockKind,
|
||||
}: {
|
||||
isToolbarVisible: boolean;
|
||||
setIsToolbarVisible: Dispatch<SetStateAction<boolean>>;
|
||||
|
|
@ -343,6 +407,7 @@ const PureToolbar = ({
|
|||
) => Promise<string | null | undefined>;
|
||||
stop: () => void;
|
||||
setMessages: Dispatch<SetStateAction<Message[]>>;
|
||||
blockKind: 'text' | 'code';
|
||||
}) => {
|
||||
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
|
@ -404,7 +469,7 @@ const PureToolbar = ({
|
|||
: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
height: 3 * 45,
|
||||
height: toolsByBlockKind[blockKind].length * 47,
|
||||
transition: { delay: 0 },
|
||||
scale: 1,
|
||||
}
|
||||
|
|
@ -461,6 +526,7 @@ const PureToolbar = ({
|
|||
selectedTool={selectedTool}
|
||||
setIsToolbarVisible={setIsToolbarVisible}
|
||||
setSelectedTool={setSelectedTool}
|
||||
blockKind={blockKind}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
|
|
@ -471,6 +537,7 @@ const PureToolbar = ({
|
|||
export const Toolbar = memo(PureToolbar, (prevProps, nextProps) => {
|
||||
if (prevProps.isLoading !== nextProps.isLoading) return false;
|
||||
if (prevProps.isToolbarVisible !== nextProps.isToolbarVisible) return false;
|
||||
if (prevProps.blockKind !== nextProps.blockKind) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ import { useSWRConfig } from 'swr';
|
|||
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
|
||||
import type { UIBlock } from './block';
|
||||
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';
|
||||
| 'user-message-id'
|
||||
| 'kind';
|
||||
|
||||
content: string | Suggestion;
|
||||
};
|
||||
|
|
@ -67,6 +69,12 @@ export function useBlockStream({
|
|||
title: delta.content as string,
|
||||
};
|
||||
|
||||
case 'kind':
|
||||
return {
|
||||
...draftBlock,
|
||||
kind: delta.content as BlockKind,
|
||||
};
|
||||
|
||||
case 'text-delta':
|
||||
return {
|
||||
...draftBlock,
|
||||
|
|
@ -80,6 +88,19 @@ export function useBlockStream({
|
|||
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) => [
|
||||
|
|
@ -107,5 +128,5 @@ export function useBlockStream({
|
|||
return draftBlock;
|
||||
}
|
||||
});
|
||||
}, [streamingData, setBlock]);
|
||||
}, [streamingData, setBlock, setUserMessageIdFromServer]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
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.
|
||||
|
||||
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.
|
||||
|
||||
**When to use \`createDocument\`:**
|
||||
- For substantial content (>10 lines)
|
||||
- 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
|
||||
|
|
@ -25,3 +28,37 @@ export const regularPrompt =
|
|||
'You are a friendly assistant! Keep your responses concise and helpful.';
|
||||
|
||||
export const systemPrompt = `${regularPrompt}\n\n${blocksPrompt}`;
|
||||
|
||||
export const codePrompt = `
|
||||
You are a Python code generator that creates self-contained, executable code snippets. When writing code:
|
||||
|
||||
1. Each snippet should be complete and runnable on its own
|
||||
2. Prefer using print() statements to display outputs
|
||||
3. Include helpful comments explaining the code
|
||||
4. Keep snippets concise (generally under 15 lines)
|
||||
5. Avoid external dependencies - use Python standard library
|
||||
6. Handle potential errors gracefully
|
||||
7. Return meaningful output that demonstrates the code's functionality
|
||||
8. Don't use input() or other interactive functions
|
||||
9. Don't access files or network resources
|
||||
10. Don't use infinite loops
|
||||
|
||||
Examples of good snippets:
|
||||
|
||||
\`\`\`python
|
||||
# Calculate factorial iteratively
|
||||
def factorial(n):
|
||||
result = 1
|
||||
for i in range(1, n + 1):
|
||||
result *= i
|
||||
return result
|
||||
|
||||
print(f"Factorial of 5 is: {factorial(5)}")
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
export const updateDocumentPrompt = (currentContent: string | null) => `\
|
||||
Update the following contents of the document based on the given prompt.
|
||||
|
||||
${currentContent}
|
||||
`;
|
||||
|
|
|
|||
1
lib/db/migrations/0004_odd_slayback.sql
Normal file
1
lib/db/migrations/0004_odd_slayback.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Document" ADD COLUMN "text" varchar DEFAULT 'text' NOT NULL;
|
||||
391
lib/db/migrations/meta/0004_snapshot.json
Normal file
391
lib/db/migrations/meta/0004_snapshot.json
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
{
|
||||
"id": "30ad8233-1432-428b-93fc-2bb1ba867ff1",
|
||||
"prevId": "011efa9e-42c7-4ff6-830a-02106f6638c9",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.Chat": {
|
||||
"name": "Chat",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"visibility": {
|
||||
"name": "visibility",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'private'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Chat_userId_User_id_fk": {
|
||||
"name": "Chat_userId_User_id_fk",
|
||||
"tableFrom": "Chat",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Document": {
|
||||
"name": "Document",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"text": {
|
||||
"name": "text",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'text'"
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Document_userId_User_id_fk": {
|
||||
"name": "Document_userId_User_id_fk",
|
||||
"tableFrom": "Document",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Document_id_createdAt_pk": {
|
||||
"name": "Document_id_createdAt_pk",
|
||||
"columns": [
|
||||
"id",
|
||||
"createdAt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Message": {
|
||||
"name": "Message",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"content": {
|
||||
"name": "content",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Message_chatId_Chat_id_fk": {
|
||||
"name": "Message_chatId_Chat_id_fk",
|
||||
"tableFrom": "Message",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Suggestion": {
|
||||
"name": "Suggestion",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"documentId": {
|
||||
"name": "documentId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"documentCreatedAt": {
|
||||
"name": "documentCreatedAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"originalText": {
|
||||
"name": "originalText",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"suggestedText": {
|
||||
"name": "suggestedText",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"isResolved": {
|
||||
"name": "isResolved",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"createdAt": {
|
||||
"name": "createdAt",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Suggestion_userId_User_id_fk": {
|
||||
"name": "Suggestion_userId_User_id_fk",
|
||||
"tableFrom": "Suggestion",
|
||||
"tableTo": "User",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
|
||||
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
|
||||
"tableFrom": "Suggestion",
|
||||
"tableTo": "Document",
|
||||
"columnsFrom": [
|
||||
"documentId",
|
||||
"documentCreatedAt"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id",
|
||||
"createdAt"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Suggestion_id_pk": {
|
||||
"name": "Suggestion_id_pk",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.User": {
|
||||
"name": "User",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "varchar(64)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.Vote": {
|
||||
"name": "Vote",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"chatId": {
|
||||
"name": "chatId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messageId": {
|
||||
"name": "messageId",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"isUpvoted": {
|
||||
"name": "isUpvoted",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"Vote_chatId_Chat_id_fk": {
|
||||
"name": "Vote_chatId_Chat_id_fk",
|
||||
"tableFrom": "Vote",
|
||||
"tableTo": "Chat",
|
||||
"columnsFrom": [
|
||||
"chatId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"Vote_messageId_Message_id_fk": {
|
||||
"name": "Vote_messageId_Message_id_fk",
|
||||
"tableFrom": "Vote",
|
||||
"tableTo": "Message",
|
||||
"columnsFrom": [
|
||||
"messageId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"Vote_chatId_messageId_pk": {
|
||||
"name": "Vote_chatId_messageId_pk",
|
||||
"columns": [
|
||||
"chatId",
|
||||
"messageId"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,13 @@
|
|||
"when": 1733403031014,
|
||||
"tag": "0003_cloudy_glorian",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "7",
|
||||
"when": 1733945232355,
|
||||
"tag": "0004_odd_slayback",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import {
|
|||
message,
|
||||
vote,
|
||||
} from './schema';
|
||||
import { BlockKind } from '@/components/block';
|
||||
|
||||
// Optionally, if not using email/pass login, you can
|
||||
// use the Drizzle adapter for Auth.js / NextAuth
|
||||
|
|
@ -169,11 +170,13 @@ export async function getVotesByChatId({ id }: { id: string }) {
|
|||
export async function saveDocument({
|
||||
id,
|
||||
title,
|
||||
kind,
|
||||
content,
|
||||
userId,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
kind: BlockKind;
|
||||
content: string;
|
||||
userId: string;
|
||||
}) {
|
||||
|
|
@ -181,6 +184,7 @@ export async function saveDocument({
|
|||
return await db.insert(document).values({
|
||||
id,
|
||||
title,
|
||||
kind,
|
||||
content,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ export const document = pgTable(
|
|||
createdAt: timestamp('createdAt').notNull(),
|
||||
title: text('title').notNull(),
|
||||
content: text('content'),
|
||||
kind: varchar('text', { enum: ['text', 'code'] })
|
||||
.notNull()
|
||||
.default('text'),
|
||||
userId: uuid('userId')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { createRoot } from 'react-dom/client';
|
|||
|
||||
import { Suggestion as PreviewSuggestion } from '@/components/suggestion';
|
||||
import type { Suggestion } from '@/lib/db/schema';
|
||||
import { BlockKind } from '@/components/block';
|
||||
|
||||
export interface UISuggestion extends Suggestion {
|
||||
selectionStart: number;
|
||||
|
|
@ -69,6 +70,7 @@ export function projectWithPositions(
|
|||
export function createSuggestionWidget(
|
||||
suggestion: UISuggestion,
|
||||
view: EditorView,
|
||||
blockKind: BlockKind = 'text',
|
||||
): { dom: HTMLElement; destroy: () => void } {
|
||||
const dom = document.createElement('span');
|
||||
const root = createRoot(dom);
|
||||
|
|
@ -111,7 +113,13 @@ export function createSuggestionWidget(
|
|||
dispatch(textTransaction);
|
||||
};
|
||||
|
||||
root.render(<PreviewSuggestion suggestion={suggestion} onApply={onApply} />);
|
||||
root.render(
|
||||
<PreviewSuggestion
|
||||
suggestion={suggestion}
|
||||
onApply={onApply}
|
||||
blockKind={blockKind}
|
||||
/>,
|
||||
);
|
||||
|
||||
return {
|
||||
dom,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "1.0.6",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lang-python": "^6.1.6",
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.35.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
|
|
@ -37,6 +42,7 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"dotenv": "^16.4.5",
|
||||
|
|
@ -62,6 +68,7 @@
|
|||
"react": "19.0.0-rc-45804af1-20241021",
|
||||
"react-dom": "19.0.0-rc-45804af1-20241021",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^1.5.0",
|
||||
|
|
|
|||
201
pnpm-lock.yaml
generated
201
pnpm-lock.yaml
generated
|
|
@ -11,6 +11,21 @@ importers:
|
|||
'@ai-sdk/openai':
|
||||
specifier: 1.0.6
|
||||
version: 1.0.6(zod@3.23.8)
|
||||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
'@codemirror/lang-python':
|
||||
specifier: ^6.1.6
|
||||
version: 6.1.6(@codemirror/view@6.35.3)
|
||||
'@codemirror/state':
|
||||
specifier: ^6.5.0
|
||||
version: 6.5.0
|
||||
'@codemirror/theme-one-dark':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
'@codemirror/view':
|
||||
specifier: ^6.35.3
|
||||
version: 6.35.3
|
||||
'@radix-ui/react-alert-dialog':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
|
||||
|
|
@ -65,6 +80,9 @@ importers:
|
|||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
codemirror:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(@lezer/common@1.2.3)
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
|
|
@ -140,6 +158,9 @@ importers:
|
|||
react-markdown:
|
||||
specifier: ^9.0.1
|
||||
version: 9.0.1(@types/react@18.3.12)(react@19.0.0-rc-45804af1-20241021)
|
||||
react-resizable-panels:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
|
|
@ -330,6 +351,41 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@codemirror/autocomplete@6.18.3':
|
||||
resolution: {integrity: sha512-1dNIOmiM0z4BIBwxmxEfA1yoxh1MF/6KPBbh20a5vphGV0ictKlgQsbJs6D6SkR6iJpGbpwRsa6PFMNlg9T9pQ==}
|
||||
peerDependencies:
|
||||
'@codemirror/language': ^6.0.0
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
'@lezer/common': ^1.0.0
|
||||
|
||||
'@codemirror/commands@6.7.1':
|
||||
resolution: {integrity: sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==}
|
||||
|
||||
'@codemirror/lang-javascript@6.2.2':
|
||||
resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==}
|
||||
|
||||
'@codemirror/lang-python@6.1.6':
|
||||
resolution: {integrity: sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==}
|
||||
|
||||
'@codemirror/language@6.10.6':
|
||||
resolution: {integrity: sha512-KrsbdCnxEztLVbB5PycWXFxas4EOyk/fPAfruSOnDDppevQgid2XZ+KbJ9u+fDikP/e7MW7HPBTvTb8JlZK9vA==}
|
||||
|
||||
'@codemirror/lint@6.8.4':
|
||||
resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==}
|
||||
|
||||
'@codemirror/search@6.5.8':
|
||||
resolution: {integrity: sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig==}
|
||||
|
||||
'@codemirror/state@6.5.0':
|
||||
resolution: {integrity: sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==}
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.2':
|
||||
resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==}
|
||||
|
||||
'@codemirror/view@6.35.3':
|
||||
resolution: {integrity: sha512-ScY7L8+EGdPl4QtoBiOzE4FELp7JmNUsBvgBcCakXWM2uiv/K89VAzU3BMDscf0DsACLvTKePbd5+cFDTcei6g==}
|
||||
|
||||
'@drizzle-team/brocli@0.10.2':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
|
|
@ -935,6 +991,24 @@ packages:
|
|||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@lezer/common@1.2.3':
|
||||
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
|
||||
|
||||
'@lezer/highlight@1.2.1':
|
||||
resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==}
|
||||
|
||||
'@lezer/javascript@1.4.21':
|
||||
resolution: {integrity: sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==}
|
||||
|
||||
'@lezer/lr@1.4.2':
|
||||
resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==}
|
||||
|
||||
'@lezer/python@1.1.15':
|
||||
resolution: {integrity: sha512-aVQ43m2zk4FZYedCqL0KHPEUsqZOrmAvRhkhHlVPnDD1HODDyyQv5BRIuod4DadkgBEZd53vQOtXTonNbEgjrQ==}
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2':
|
||||
resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
|
||||
|
||||
'@neondatabase/serverless@0.9.5':
|
||||
resolution: {integrity: sha512-siFas6gItqv6wD/pZnvdu34wEqgG3nSE6zWZdq5j2DEsa+VvX8i/5HXJOo06qrw5axPXn+lGCxeR+NLaSPIXug==}
|
||||
|
||||
|
|
@ -1743,6 +1817,9 @@ packages:
|
|||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
codemirror@6.0.1:
|
||||
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
|
@ -3168,6 +3245,12 @@ packages:
|
|||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-resizable-panels@2.1.7:
|
||||
resolution: {integrity: sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==}
|
||||
peerDependencies:
|
||||
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
react-style-singleton@2.2.1:
|
||||
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -3377,6 +3460,9 @@ packages:
|
|||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
style-mod@4.1.2:
|
||||
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==}
|
||||
|
||||
|
|
@ -3731,6 +3817,78 @@ snapshots:
|
|||
'@biomejs/cli-win32-x64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@codemirror/autocomplete@6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/commands@6.7.1':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@codemirror/lang-javascript@6.2.2':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/lint': 6.8.4
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/javascript': 1.4.21
|
||||
|
||||
'@codemirror/lang-python@6.1.6(@codemirror/view@6.35.3)':
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/state': 6.5.0
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/python': 1.1.15
|
||||
transitivePeerDependencies:
|
||||
- '@codemirror/view'
|
||||
|
||||
'@codemirror/language@6.10.6':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
style-mod: 4.1.2
|
||||
|
||||
'@codemirror/lint@6.8.4':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/search@6.5.8':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
crelt: 1.0.6
|
||||
|
||||
'@codemirror/state@6.5.0':
|
||||
dependencies:
|
||||
'@marijn/find-cluster-break': 1.0.2
|
||||
|
||||
'@codemirror/theme-one-dark@6.1.2':
|
||||
dependencies:
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
|
||||
'@codemirror/view@6.35.3':
|
||||
dependencies:
|
||||
'@codemirror/state': 6.5.0
|
||||
style-mod: 4.1.2
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
|
||||
'@emnapi/runtime@1.3.1':
|
||||
|
|
@ -4110,6 +4268,30 @@ snapshots:
|
|||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
'@lezer/common@1.2.3': {}
|
||||
|
||||
'@lezer/highlight@1.2.1':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@lezer/javascript@1.4.21':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@lezer/lr@1.4.2':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
|
||||
'@lezer/python@1.1.15':
|
||||
dependencies:
|
||||
'@lezer/common': 1.2.3
|
||||
'@lezer/highlight': 1.2.1
|
||||
'@lezer/lr': 1.4.2
|
||||
|
||||
'@marijn/find-cluster-break@1.0.2': {}
|
||||
|
||||
'@neondatabase/serverless@0.9.5':
|
||||
dependencies:
|
||||
'@types/pg': 8.11.6
|
||||
|
|
@ -4899,6 +5081,18 @@ snapshots:
|
|||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
codemirror@6.0.1(@lezer/common@1.2.3):
|
||||
dependencies:
|
||||
'@codemirror/autocomplete': 6.18.3(@codemirror/language@6.10.6)(@codemirror/state@6.5.0)(@codemirror/view@6.35.3)(@lezer/common@1.2.3)
|
||||
'@codemirror/commands': 6.7.1
|
||||
'@codemirror/language': 6.10.6
|
||||
'@codemirror/lint': 6.8.4
|
||||
'@codemirror/search': 6.5.8
|
||||
'@codemirror/state': 6.5.0
|
||||
'@codemirror/view': 6.35.3
|
||||
transitivePeerDependencies:
|
||||
- '@lezer/common'
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
|
@ -6681,6 +6875,11 @@ snapshots:
|
|||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
react-resizable-panels@2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021):
|
||||
dependencies:
|
||||
react: 19.0.0-rc-45804af1-20241021
|
||||
react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021)
|
||||
|
||||
react-style-singleton@2.2.1(@types/react@18.3.12)(react@19.0.0-rc-45804af1-20241021):
|
||||
dependencies:
|
||||
get-nonce: 1.0.1
|
||||
|
|
@ -6961,6 +7160,8 @@ snapshots:
|
|||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
style-mod@4.1.2: {}
|
||||
|
||||
style-to-object@1.0.8:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.4
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue