Skip to content

Commit 38527ff

Browse files
feat: modularize block system (#718)
1 parent 5e8cddc commit 38527ff

18 files changed

+684
-604
lines changed

blocks/actions.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
'use server';
2+
3+
import { getSuggestionsByDocumentId } from '@/lib/db/queries';
4+
5+
export async function getSuggestions({ documentId }: { documentId: string }) {
6+
const suggestions = await getSuggestionsByDocumentId({ documentId });
7+
return suggestions;
8+
}

blocks/code.tsx

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { Block } from '@/components/create-block';
2+
import { CodeEditor } from '@/components/code-editor';
3+
import {
4+
CopyIcon,
5+
LogsIcon,
6+
MessageIcon,
7+
PlayIcon,
8+
RedoIcon,
9+
UndoIcon,
10+
} from '@/components/icons';
11+
import { toast } from 'sonner';
12+
import { generateUUID } from '@/lib/utils';
13+
import { Console, ConsoleOutput } from '@/components/console';
14+
15+
interface Metadata {
16+
outputs: Array<ConsoleOutput>;
17+
}
18+
19+
export const codeBlock = new Block<'code', Metadata>({
20+
kind: 'code',
21+
description:
22+
'Useful for code generation; Code execution is only available for python code.',
23+
initialize: () => ({
24+
outputs: [],
25+
}),
26+
onStreamPart: ({ streamPart, setBlock }) => {
27+
if (streamPart.type === 'code-delta') {
28+
setBlock((draftBlock) => ({
29+
...draftBlock,
30+
content: streamPart.content as string,
31+
isVisible:
32+
draftBlock.status === 'streaming' &&
33+
draftBlock.content.length > 300 &&
34+
draftBlock.content.length < 310
35+
? true
36+
: draftBlock.isVisible,
37+
status: 'streaming',
38+
}));
39+
}
40+
},
41+
content: ({ metadata, setMetadata, ...props }) => {
42+
return (
43+
<>
44+
<CodeEditor {...props} />
45+
46+
{metadata?.outputs && (
47+
<Console
48+
consoleOutputs={metadata.outputs}
49+
setConsoleOutputs={() => {
50+
setMetadata({
51+
...metadata,
52+
outputs: [],
53+
});
54+
}}
55+
/>
56+
)}
57+
</>
58+
);
59+
},
60+
actions: [
61+
{
62+
icon: <PlayIcon size={18} />,
63+
label: 'Run',
64+
description: 'Execute code',
65+
onClick: async ({ content, setMetadata }) => {
66+
const runId = generateUUID();
67+
const outputs: any[] = [];
68+
69+
// @ts-expect-error - loadPyodide is not defined
70+
const currentPyodideInstance = await globalThis.loadPyodide({
71+
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/',
72+
});
73+
74+
currentPyodideInstance.setStdout({
75+
batched: (output: string) => {
76+
outputs.push({
77+
id: runId,
78+
contents: [
79+
{
80+
type: output.startsWith('data:image/png;base64')
81+
? 'image'
82+
: 'text',
83+
value: output,
84+
},
85+
],
86+
status: 'completed',
87+
});
88+
},
89+
});
90+
91+
await currentPyodideInstance.loadPackagesFromImports(content, {
92+
messageCallback: (message: string) => {
93+
outputs.push({
94+
id: runId,
95+
contents: [{ type: 'text', value: message }],
96+
status: 'loading_packages',
97+
});
98+
},
99+
});
100+
101+
await currentPyodideInstance.runPythonAsync(content);
102+
103+
setMetadata((metadata: any) => ({
104+
...metadata,
105+
outputs,
106+
}));
107+
},
108+
},
109+
{
110+
icon: <UndoIcon size={18} />,
111+
description: 'View Previous version',
112+
onClick: ({ handleVersionChange }) => {
113+
handleVersionChange('prev');
114+
},
115+
},
116+
{
117+
icon: <RedoIcon size={18} />,
118+
description: 'View Next version',
119+
onClick: ({ handleVersionChange }) => {
120+
handleVersionChange('next');
121+
},
122+
},
123+
{
124+
icon: <CopyIcon size={18} />,
125+
description: 'Copy code to clipboard',
126+
onClick: ({ content }) => {
127+
navigator.clipboard.writeText(content);
128+
toast.success('Copied to clipboard!');
129+
},
130+
},
131+
],
132+
toolbar: [
133+
{
134+
icon: <MessageIcon />,
135+
description: 'Add comments',
136+
onClick: ({ appendMessage }) => {
137+
appendMessage({
138+
role: 'user',
139+
content: 'Add comments to the code snippet for understanding',
140+
});
141+
},
142+
},
143+
{
144+
icon: <LogsIcon />,
145+
description: 'Add logs',
146+
onClick: ({ appendMessage }) => {
147+
appendMessage({
148+
role: 'user',
149+
content: 'Add logs to the code snippet for debugging',
150+
});
151+
},
152+
},
153+
],
154+
});

blocks/image.tsx

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Block } from '@/components/create-block';
2+
import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons';
3+
import { ImageEditor } from '@/components/image-editor';
4+
5+
export const imageBlock = new Block({
6+
kind: 'image',
7+
description: 'Useful for image generation',
8+
onStreamPart: ({ streamPart, setBlock }) => {
9+
if (streamPart.type === 'image-delta') {
10+
setBlock((draftBlock) => ({
11+
...draftBlock,
12+
content: streamPart.content as string,
13+
isVisible: true,
14+
status: 'streaming',
15+
}));
16+
}
17+
},
18+
content: ImageEditor,
19+
actions: [
20+
{
21+
icon: <UndoIcon size={18} />,
22+
description: 'View Previous version',
23+
onClick: ({ handleVersionChange }) => {
24+
handleVersionChange('prev');
25+
},
26+
},
27+
{
28+
icon: <RedoIcon size={18} />,
29+
description: 'View Next version',
30+
onClick: ({ handleVersionChange }) => {
31+
handleVersionChange('next');
32+
},
33+
},
34+
{
35+
icon: <CopyIcon size={18} />,
36+
description: 'Copy image to clipboard',
37+
onClick: ({ content }) => {
38+
const img = new Image();
39+
img.src = `data:image/png;base64,${content}`;
40+
41+
img.onload = () => {
42+
const canvas = document.createElement('canvas');
43+
canvas.width = img.width;
44+
canvas.height = img.height;
45+
const ctx = canvas.getContext('2d');
46+
ctx?.drawImage(img, 0, 0);
47+
canvas.toBlob((blob) => {
48+
if (blob) {
49+
navigator.clipboard.write([
50+
new ClipboardItem({ 'image/png': blob }),
51+
]);
52+
}
53+
}, 'image/png');
54+
};
55+
},
56+
},
57+
],
58+
toolbar: [],
59+
});

blocks/text.tsx

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { Block } from '@/components/create-block';
2+
import { DiffView } from '@/components/diffview';
3+
import { DocumentSkeleton } from '@/components/document-skeleton';
4+
import { Editor } from '@/components/editor';
5+
import {
6+
ClockRewind,
7+
CopyIcon,
8+
MessageIcon,
9+
PenIcon,
10+
RedoIcon,
11+
UndoIcon,
12+
} from '@/components/icons';
13+
import { Suggestion } from '@/lib/db/schema';
14+
import { toast } from 'sonner';
15+
import { getSuggestions } from './actions';
16+
17+
interface TextBlockMetadata {
18+
suggestions: Array<Suggestion>;
19+
}
20+
21+
export const textBlock = new Block<'text', TextBlockMetadata>({
22+
kind: 'text',
23+
description: 'Useful for text content, like drafting essays and emails.',
24+
initialize: async ({ documentId, setMetadata }) => {
25+
const suggestions = await getSuggestions({ documentId });
26+
27+
setMetadata({
28+
suggestions,
29+
});
30+
},
31+
onStreamPart: ({ streamPart, setMetadata, setBlock }) => {
32+
if (streamPart.type === 'suggestion') {
33+
setMetadata((metadata) => {
34+
return {
35+
suggestions: [
36+
...metadata.suggestions,
37+
streamPart.content as Suggestion,
38+
],
39+
};
40+
});
41+
}
42+
43+
if (streamPart.type === 'text-delta') {
44+
setBlock((draftBlock) => {
45+
return {
46+
...draftBlock,
47+
content: draftBlock.content + (streamPart.content as string),
48+
isVisible:
49+
draftBlock.status === 'streaming' &&
50+
draftBlock.content.length > 400 &&
51+
draftBlock.content.length < 450
52+
? true
53+
: draftBlock.isVisible,
54+
status: 'streaming',
55+
};
56+
});
57+
}
58+
},
59+
content: ({
60+
mode,
61+
status,
62+
content,
63+
isCurrentVersion,
64+
currentVersionIndex,
65+
onSaveContent,
66+
getDocumentContentById,
67+
isLoading,
68+
metadata,
69+
}) => {
70+
if (isLoading) {
71+
return <DocumentSkeleton blockKind="text" />;
72+
}
73+
74+
if (mode === 'diff') {
75+
const oldContent = getDocumentContentById(currentVersionIndex - 1);
76+
const newContent = getDocumentContentById(currentVersionIndex);
77+
78+
return <DiffView oldContent={oldContent} newContent={newContent} />;
79+
}
80+
81+
return (
82+
<>
83+
<Editor
84+
content={content}
85+
suggestions={metadata ? metadata.suggestions : []}
86+
isCurrentVersion={isCurrentVersion}
87+
currentVersionIndex={currentVersionIndex}
88+
status={status}
89+
onSaveContent={onSaveContent}
90+
/>
91+
92+
{metadata && metadata.suggestions && metadata.suggestions.length > 0 ? (
93+
<div className="md:hidden h-dvh w-12 shrink-0" />
94+
) : null}
95+
</>
96+
);
97+
},
98+
actions: [
99+
{
100+
icon: <ClockRewind size={18} />,
101+
description: 'View changes',
102+
onClick: ({ handleVersionChange }) => {
103+
handleVersionChange('toggle');
104+
},
105+
isDisabled: ({ currentVersionIndex, setMetadata }) => {
106+
if (currentVersionIndex === 0) {
107+
return true;
108+
}
109+
110+
return false;
111+
},
112+
},
113+
{
114+
icon: <UndoIcon size={18} />,
115+
description: 'View Previous version',
116+
onClick: ({ handleVersionChange }) => {
117+
handleVersionChange('prev');
118+
},
119+
isDisabled: ({ currentVersionIndex }) => {
120+
if (currentVersionIndex === 0) {
121+
return true;
122+
}
123+
124+
return false;
125+
},
126+
},
127+
{
128+
icon: <RedoIcon size={18} />,
129+
description: 'View Next version',
130+
onClick: ({ handleVersionChange }) => {
131+
handleVersionChange('next');
132+
},
133+
isDisabled: ({ isCurrentVersion }) => {
134+
if (isCurrentVersion) {
135+
return true;
136+
}
137+
138+
return false;
139+
},
140+
},
141+
{
142+
icon: <CopyIcon size={18} />,
143+
description: 'Copy to clipboard',
144+
onClick: ({ content }) => {
145+
navigator.clipboard.writeText(content);
146+
toast.success('Copied to clipboard!');
147+
},
148+
},
149+
],
150+
toolbar: [
151+
{
152+
icon: <PenIcon />,
153+
description: 'Add final polish',
154+
onClick: ({ appendMessage }) => {
155+
appendMessage({
156+
role: 'user',
157+
content:
158+
'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.',
159+
});
160+
},
161+
},
162+
{
163+
icon: <MessageIcon />,
164+
description: 'Request suggestions',
165+
onClick: ({ appendMessage }) => {
166+
appendMessage({
167+
role: 'user',
168+
content:
169+
'Please add suggestions you have that could improve the writing.',
170+
});
171+
},
172+
},
173+
],
174+
});

0 commit comments

Comments
 (0)