forked from vercel/ai-chatbot
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: modularize block system (vercel#718)
- Loading branch information
1 parent
5e8cddc
commit 38527ff
Showing
18 changed files
with
684 additions
and
604 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
'use server'; | ||
|
||
import { getSuggestionsByDocumentId } from '@/lib/db/queries'; | ||
|
||
export async function getSuggestions({ documentId }: { documentId: string }) { | ||
const suggestions = await getSuggestionsByDocumentId({ documentId }); | ||
return suggestions; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import { Block } from '@/components/create-block'; | ||
import { CodeEditor } from '@/components/code-editor'; | ||
import { | ||
CopyIcon, | ||
LogsIcon, | ||
MessageIcon, | ||
PlayIcon, | ||
RedoIcon, | ||
UndoIcon, | ||
} from '@/components/icons'; | ||
import { toast } from 'sonner'; | ||
import { generateUUID } from '@/lib/utils'; | ||
import { Console, ConsoleOutput } from '@/components/console'; | ||
|
||
interface Metadata { | ||
outputs: Array<ConsoleOutput>; | ||
} | ||
|
||
export const codeBlock = new Block<'code', Metadata>({ | ||
kind: 'code', | ||
description: | ||
'Useful for code generation; Code execution is only available for python code.', | ||
initialize: () => ({ | ||
outputs: [], | ||
}), | ||
onStreamPart: ({ streamPart, setBlock }) => { | ||
if (streamPart.type === 'code-delta') { | ||
setBlock((draftBlock) => ({ | ||
...draftBlock, | ||
content: streamPart.content as string, | ||
isVisible: | ||
draftBlock.status === 'streaming' && | ||
draftBlock.content.length > 300 && | ||
draftBlock.content.length < 310 | ||
? true | ||
: draftBlock.isVisible, | ||
status: 'streaming', | ||
})); | ||
} | ||
}, | ||
content: ({ metadata, setMetadata, ...props }) => { | ||
return ( | ||
<> | ||
<CodeEditor {...props} /> | ||
|
||
{metadata?.outputs && ( | ||
<Console | ||
consoleOutputs={metadata.outputs} | ||
setConsoleOutputs={() => { | ||
setMetadata({ | ||
...metadata, | ||
outputs: [], | ||
}); | ||
}} | ||
/> | ||
)} | ||
</> | ||
); | ||
}, | ||
actions: [ | ||
{ | ||
icon: <PlayIcon size={18} />, | ||
label: 'Run', | ||
description: 'Execute code', | ||
onClick: async ({ content, setMetadata }) => { | ||
const runId = generateUUID(); | ||
const outputs: any[] = []; | ||
|
||
// @ts-expect-error - loadPyodide is not defined | ||
const currentPyodideInstance = await globalThis.loadPyodide({ | ||
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.23.4/full/', | ||
}); | ||
|
||
currentPyodideInstance.setStdout({ | ||
batched: (output: string) => { | ||
outputs.push({ | ||
id: runId, | ||
contents: [ | ||
{ | ||
type: output.startsWith('data:image/png;base64') | ||
? 'image' | ||
: 'text', | ||
value: output, | ||
}, | ||
], | ||
status: 'completed', | ||
}); | ||
}, | ||
}); | ||
|
||
await currentPyodideInstance.loadPackagesFromImports(content, { | ||
messageCallback: (message: string) => { | ||
outputs.push({ | ||
id: runId, | ||
contents: [{ type: 'text', value: message }], | ||
status: 'loading_packages', | ||
}); | ||
}, | ||
}); | ||
|
||
await currentPyodideInstance.runPythonAsync(content); | ||
|
||
setMetadata((metadata: any) => ({ | ||
...metadata, | ||
outputs, | ||
})); | ||
}, | ||
}, | ||
{ | ||
icon: <UndoIcon size={18} />, | ||
description: 'View Previous version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('prev'); | ||
}, | ||
}, | ||
{ | ||
icon: <RedoIcon size={18} />, | ||
description: 'View Next version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('next'); | ||
}, | ||
}, | ||
{ | ||
icon: <CopyIcon size={18} />, | ||
description: 'Copy code to clipboard', | ||
onClick: ({ content }) => { | ||
navigator.clipboard.writeText(content); | ||
toast.success('Copied to clipboard!'); | ||
}, | ||
}, | ||
], | ||
toolbar: [ | ||
{ | ||
icon: <MessageIcon />, | ||
description: 'Add comments', | ||
onClick: ({ appendMessage }) => { | ||
appendMessage({ | ||
role: 'user', | ||
content: 'Add comments to the code snippet for understanding', | ||
}); | ||
}, | ||
}, | ||
{ | ||
icon: <LogsIcon />, | ||
description: 'Add logs', | ||
onClick: ({ appendMessage }) => { | ||
appendMessage({ | ||
role: 'user', | ||
content: 'Add logs to the code snippet for debugging', | ||
}); | ||
}, | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import { Block } from '@/components/create-block'; | ||
import { CopyIcon, RedoIcon, UndoIcon } from '@/components/icons'; | ||
import { ImageEditor } from '@/components/image-editor'; | ||
|
||
export const imageBlock = new Block({ | ||
kind: 'image', | ||
description: 'Useful for image generation', | ||
onStreamPart: ({ streamPart, setBlock }) => { | ||
if (streamPart.type === 'image-delta') { | ||
setBlock((draftBlock) => ({ | ||
...draftBlock, | ||
content: streamPart.content as string, | ||
isVisible: true, | ||
status: 'streaming', | ||
})); | ||
} | ||
}, | ||
content: ImageEditor, | ||
actions: [ | ||
{ | ||
icon: <UndoIcon size={18} />, | ||
description: 'View Previous version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('prev'); | ||
}, | ||
}, | ||
{ | ||
icon: <RedoIcon size={18} />, | ||
description: 'View Next version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('next'); | ||
}, | ||
}, | ||
{ | ||
icon: <CopyIcon size={18} />, | ||
description: 'Copy image to clipboard', | ||
onClick: ({ content }) => { | ||
const img = new Image(); | ||
img.src = `data:image/png;base64,${content}`; | ||
|
||
img.onload = () => { | ||
const canvas = document.createElement('canvas'); | ||
canvas.width = img.width; | ||
canvas.height = img.height; | ||
const ctx = canvas.getContext('2d'); | ||
ctx?.drawImage(img, 0, 0); | ||
canvas.toBlob((blob) => { | ||
if (blob) { | ||
navigator.clipboard.write([ | ||
new ClipboardItem({ 'image/png': blob }), | ||
]); | ||
} | ||
}, 'image/png'); | ||
}; | ||
}, | ||
}, | ||
], | ||
toolbar: [], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { Block } from '@/components/create-block'; | ||
import { DiffView } from '@/components/diffview'; | ||
import { DocumentSkeleton } from '@/components/document-skeleton'; | ||
import { Editor } from '@/components/editor'; | ||
import { | ||
ClockRewind, | ||
CopyIcon, | ||
MessageIcon, | ||
PenIcon, | ||
RedoIcon, | ||
UndoIcon, | ||
} from '@/components/icons'; | ||
import { Suggestion } from '@/lib/db/schema'; | ||
import { toast } from 'sonner'; | ||
import { getSuggestions } from './actions'; | ||
|
||
interface TextBlockMetadata { | ||
suggestions: Array<Suggestion>; | ||
} | ||
|
||
export const textBlock = new Block<'text', TextBlockMetadata>({ | ||
kind: 'text', | ||
description: 'Useful for text content, like drafting essays and emails.', | ||
initialize: async ({ documentId, setMetadata }) => { | ||
const suggestions = await getSuggestions({ documentId }); | ||
|
||
setMetadata({ | ||
suggestions, | ||
}); | ||
}, | ||
onStreamPart: ({ streamPart, setMetadata, setBlock }) => { | ||
if (streamPart.type === 'suggestion') { | ||
setMetadata((metadata) => { | ||
return { | ||
suggestions: [ | ||
...metadata.suggestions, | ||
streamPart.content as Suggestion, | ||
], | ||
}; | ||
}); | ||
} | ||
|
||
if (streamPart.type === 'text-delta') { | ||
setBlock((draftBlock) => { | ||
return { | ||
...draftBlock, | ||
content: draftBlock.content + (streamPart.content as string), | ||
isVisible: | ||
draftBlock.status === 'streaming' && | ||
draftBlock.content.length > 400 && | ||
draftBlock.content.length < 450 | ||
? true | ||
: draftBlock.isVisible, | ||
status: 'streaming', | ||
}; | ||
}); | ||
} | ||
}, | ||
content: ({ | ||
mode, | ||
status, | ||
content, | ||
isCurrentVersion, | ||
currentVersionIndex, | ||
onSaveContent, | ||
getDocumentContentById, | ||
isLoading, | ||
metadata, | ||
}) => { | ||
if (isLoading) { | ||
return <DocumentSkeleton blockKind="text" />; | ||
} | ||
|
||
if (mode === 'diff') { | ||
const oldContent = getDocumentContentById(currentVersionIndex - 1); | ||
const newContent = getDocumentContentById(currentVersionIndex); | ||
|
||
return <DiffView oldContent={oldContent} newContent={newContent} />; | ||
} | ||
|
||
return ( | ||
<> | ||
<Editor | ||
content={content} | ||
suggestions={metadata ? metadata.suggestions : []} | ||
isCurrentVersion={isCurrentVersion} | ||
currentVersionIndex={currentVersionIndex} | ||
status={status} | ||
onSaveContent={onSaveContent} | ||
/> | ||
|
||
{metadata && metadata.suggestions && metadata.suggestions.length > 0 ? ( | ||
<div className="md:hidden h-dvh w-12 shrink-0" /> | ||
) : null} | ||
</> | ||
); | ||
}, | ||
actions: [ | ||
{ | ||
icon: <ClockRewind size={18} />, | ||
description: 'View changes', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('toggle'); | ||
}, | ||
isDisabled: ({ currentVersionIndex, setMetadata }) => { | ||
if (currentVersionIndex === 0) { | ||
return true; | ||
} | ||
|
||
return false; | ||
}, | ||
}, | ||
{ | ||
icon: <UndoIcon size={18} />, | ||
description: 'View Previous version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('prev'); | ||
}, | ||
isDisabled: ({ currentVersionIndex }) => { | ||
if (currentVersionIndex === 0) { | ||
return true; | ||
} | ||
|
||
return false; | ||
}, | ||
}, | ||
{ | ||
icon: <RedoIcon size={18} />, | ||
description: 'View Next version', | ||
onClick: ({ handleVersionChange }) => { | ||
handleVersionChange('next'); | ||
}, | ||
isDisabled: ({ isCurrentVersion }) => { | ||
if (isCurrentVersion) { | ||
return true; | ||
} | ||
|
||
return false; | ||
}, | ||
}, | ||
{ | ||
icon: <CopyIcon size={18} />, | ||
description: 'Copy to clipboard', | ||
onClick: ({ content }) => { | ||
navigator.clipboard.writeText(content); | ||
toast.success('Copied to clipboard!'); | ||
}, | ||
}, | ||
], | ||
toolbar: [ | ||
{ | ||
icon: <PenIcon />, | ||
description: 'Add final polish', | ||
onClick: ({ appendMessage }) => { | ||
appendMessage({ | ||
role: 'user', | ||
content: | ||
'Please add final polish and check for grammar, add section titles for better structure, and ensure everything reads smoothly.', | ||
}); | ||
}, | ||
}, | ||
{ | ||
icon: <MessageIcon />, | ||
description: 'Request suggestions', | ||
onClick: ({ appendMessage }) => { | ||
appendMessage({ | ||
role: 'user', | ||
content: | ||
'Please add suggestions you have that could improve the writing.', | ||
}); | ||
}, | ||
}, | ||
], | ||
}); |
Oops, something went wrong.