feat: add code and text block types (#609)

This commit is contained in:
Jeremy 2024-12-16 18:14:40 +05:30 committed by GitHub
parent 3df0fd4c0f
commit 9778631d6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1754 additions and 290 deletions

View file

@ -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,11 +143,17 @@ export async function POST(request: Request) {
content: title,
});
streamingData.append({
type: 'kind',
content: kind,
});
streamingData.append({
type: 'clear',
content: '',
});
if (kind === 'text') {
const { fullStream } = streamText({
model: customModel(model.apiIdentifier),
system:
@ -164,11 +176,42 @@ export async function POST(request: Request) {
}
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,10 +250,11 @@ export async function POST(request: Request) {
content: document.title,
});
if (document.kind === 'text') {
const { fullStream } = streamText({
model: customModel(model.apiIdentifier),
system:
'You are a helpful writing assistant. Based on the description, please update the piece of writing.',
system: updateDocumentPrompt(currentContent),
prompt: description,
experimental_providerMetadata: {
openai: {
prediction: {
@ -218,13 +263,6 @@ export async function POST(request: Request) {
},
},
},
messages: [
{
role: 'user',
content: description,
},
{ role: 'user', content: currentContent },
],
});
for await (const delta of fullStream) {
@ -242,12 +280,43 @@ export async function POST(request: Request) {
}
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',
};
},

View file

@ -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,
});

View file

@ -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 (
<>
<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>
</>
);
}

View file

@ -141,6 +141,38 @@
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;
}

View file

@ -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>
);
}

View file

@ -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,14 +430,42 @@ 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' ? (
) : block.kind === 'code' ? (
<CodeEditor
content={
isCurrentVersion
? block.content
: getDocumentContentById(currentVersionIndex)
}
isCurrentVersion={isCurrentVersion}
currentVersionIndex={currentVersionIndex}
suggestions={suggestions ?? []}
status={block.status}
saveContent={saveContent}
/>
) : block.kind === 'text' ? (
mode === 'edit' ? (
<Editor
content={
isCurrentVersion
@ -440,7 +483,8 @@ function PureBlock({
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>
);

View file

@ -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
View 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
View 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
View 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;
}

View file

@ -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,

View file

@ -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>
);
};

View file

@ -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}>

View file

@ -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>
)}

View file

@ -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!',
},
];

View file

@ -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 }}

View file

@ -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;
});

View file

@ -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]);
}

View file

@ -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}
`;

View file

@ -0,0 +1 @@
ALTER TABLE "Document" ADD COLUMN "text" varchar DEFAULT 'text' NOT NULL;

View 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": {}
}
}

View file

@ -29,6 +29,13 @@
"when": 1733403031014,
"tag": "0003_cloudy_glorian",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1733945232355,
"tag": "0004_odd_slayback",
"breakpoints": true
}
]
}

View file

@ -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(),

View file

@ -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),

View file

@ -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,

View file

@ -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
View file

@ -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