Skip to content

Commit 1a18f04

Browse files
authored
Implement inclusion of prompts into other prompts by typing "@" (#1434)
1 parent f6653d6 commit 1a18f04

File tree

12 files changed

+706
-84
lines changed

12 files changed

+706
-84
lines changed

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/BlocksEditor/index.tsx

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
import { memo, Suspense } from 'react'
1+
import { memo, Suspense, useCallback } from 'react'
2+
import Link from 'next/link'
23
import { AstError, AnyBlock } from '@latitude-data/constants/simpleBlocks'
34
import { TextEditorPlaceholder } from '@latitude-data/web-ui/molecules/TextEditorPlaceholder'
4-
import { BlocksEditor } from '@latitude-data/web-ui/molecules/BlocksEditor'
5+
import {
6+
BlocksEditor,
7+
IncludedPrompt,
8+
} from '@latitude-data/web-ui/molecules/BlocksEditor'
9+
import {
10+
ICommitContextType,
11+
IProjectContextType,
12+
} from '@latitude-data/web-ui/providers'
13+
import { type DocumentVersion } from '@latitude-data/core/browser'
14+
import { useIncludabledPrompts } from './useIncludabledPrompts'
15+
import { scan } from 'promptl-ai'
16+
import useDocumentVersions from '$/stores/documentVersions'
17+
import { ReactStateDispatch } from '@latitude-data/web-ui/commonTypes'
518

619
// Example blocks to demonstrate the editor
720
const exampleBlocks: AnyBlock[] = [
@@ -62,46 +75,64 @@ const exampleBlocks: AnyBlock[] = [
6275

6376
export const PlaygroundBlocksEditor = memo(
6477
({
78+
project,
79+
commit,
80+
document,
81+
onToggleBlocksEditor,
6582
value: _prompt,
6683
}: {
84+
project: IProjectContextType['project']
85+
commit: ICommitContextType['commit']
86+
document: DocumentVersion
6787
compileErrors: AstError[] | undefined
6888
blocks: AnyBlock[] | undefined
6989
value: string
7090
defaultValue?: string
7191
isSaved: boolean
7292
readOnlyMessage?: string
93+
onToggleBlocksEditor: ReactStateDispatch<boolean>
7394
onChange: (value: string) => void
7495
}) => {
96+
const { data: documents } = useDocumentVersions({
97+
commitUuid: commit?.uuid,
98+
projectId: project?.id,
99+
})
100+
const prompts = useIncludabledPrompts({
101+
project,
102+
commit,
103+
document,
104+
documents,
105+
})
75106
// const blocksToRender = blocks.length > 0 ? blocks : exampleBlocks
76107

77108
const handleBlocksChange = (_updatedBlocks: AnyBlock[]) => {
78109
// Convert blocks back to string format if needed
79110
// For now, we'll just stringify the blocks
80111
// onChange(JSON.stringify(updatedBlocks, null, 2))
81112
}
82-
113+
const onRequestPromptMetadata = useCallback(
114+
async ({ id }: IncludedPrompt) => {
115+
const prompt = documents.find((doc) => doc.id === id)
116+
return await scan({ prompt: prompt!.content })
117+
},
118+
[documents],
119+
)
120+
const onToogleDevEditor = useCallback(() => {
121+
onToggleBlocksEditor(false)
122+
}, [onToggleBlocksEditor])
83123
return (
84124
<Suspense fallback={<TextEditorPlaceholder />}>
85-
<div className='space-y-4'>
86-
<div className='text-sm text-gray-600'>
87-
Blocks Editor Demo - {exampleBlocks.length} blocks loaded
88-
</div>
89-
<BlocksEditor
90-
autoFocus
91-
readOnly={false}
92-
initialValue={exampleBlocks}
93-
onBlocksChange={handleBlocksChange}
94-
placeholder='Write your prompt, type "/" to insert messages or steps, "@" for include other prompts, "{{" for variables, Try typing "{{my_variable}}"'
95-
/>
96-
<details className='text-xs'>
97-
<summary className='cursor-pointer text-gray-500'>
98-
Show raw blocks data
99-
</summary>
100-
<pre className='bg-gray-100 p-2 rounded mt-2 overflow-auto max-h-40'>
101-
{JSON.stringify(exampleBlocks, null, 2)}
102-
</pre>
103-
</details>
104-
</div>
125+
<BlocksEditor
126+
autoFocus
127+
readOnly={false}
128+
prompts={prompts}
129+
initialValue={exampleBlocks}
130+
Link={Link}
131+
onRequestPromptMetadata={onRequestPromptMetadata}
132+
onToggleDevEditor={onToogleDevEditor}
133+
onBlocksChange={handleBlocksChange}
134+
placeholder='Write your prompt, type "/" to insert messages or steps, "@" for include other prompts, "{{" for variables, Try typing "{{my_variable}}"'
135+
/>
105136
</Suspense>
106137
)
107138
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {
2+
ICommitContextType,
3+
IProjectContextType,
4+
} from '@latitude-data/web-ui/providers'
5+
import { type DocumentVersion, DocumentType } from '@latitude-data/core/browser'
6+
import { useMemo } from 'react'
7+
import { ROUTES } from '$/services/routes'
8+
import { IncludedPrompt } from '@latitude-data/web-ui/molecules/BlocksEditor'
9+
10+
const docUrl = (projectId: number, commitUuid: string, uuid: string) =>
11+
ROUTES.projects
12+
.detail({ id: projectId })
13+
.commits.detail({ uuid: commitUuid })
14+
.documents.detail({ uuid }).root
15+
16+
/**
17+
* This hook generates the prompts in the sidebar that:
18+
* 1. Are not the current document
19+
* 2. Are of type `DocumentType.Prompt`
20+
*
21+
* This is used for the blocks editor and does not makes sense to include
22+
* documents of type `DocumentType.Agent`
23+
*/
24+
export function useIncludabledPrompts({
25+
project,
26+
commit,
27+
document,
28+
documents,
29+
}: {
30+
project: IProjectContextType['project']
31+
commit: ICommitContextType['commit']
32+
document: DocumentVersion
33+
documents: DocumentVersion[]
34+
}) {
35+
return useMemo(() => {
36+
return documents
37+
.filter(
38+
(doc) =>
39+
doc.id !== document.id && doc.documentType === DocumentType.Prompt,
40+
)
41+
.reduce(
42+
(acc, doc) => {
43+
acc[doc.path] = {
44+
url: docUrl(project.id, commit.uuid, doc.documentUuid),
45+
id: doc.id,
46+
path: doc.path,
47+
projectId: project.id,
48+
commitUuid: commit.uuid,
49+
documentUuid: doc.documentUuid,
50+
}
51+
return acc
52+
},
53+
{} as Record<string, IncludedPrompt>,
54+
)
55+
}, [document, documents, project.id, commit.uuid])
56+
}

apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/_components/DocumentEditor/Editor/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,12 @@ export default function DocumentEditor({
320320
) : null}
321321
{showBlocksEditor ? (
322322
<PlaygroundBlocksEditor
323+
project={project}
324+
document={document}
325+
commit={commit}
323326
readOnlyMessage={readOnlyMessage}
324327
isSaved={!isUpdatingContent}
328+
onToggleBlocksEditor={setBlockEditorVisible}
325329
defaultValue={document.content}
326330
value={value}
327331
blocks={metadata?.blocks}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export type { AnyBlock, AstError } from './types'
1+
export type { AnyBlock, AstError, PromptBlock } from './types'
22
export * from './astToSimpleBlocks'
33
export * from './simpleBlocksToText'
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createContext, ReactNode, useContext, useRef } from 'react'
2+
import { BlocksEditorProps } from '../../types'
3+
4+
type IBlocksProvider = {
5+
prompts: BlocksEditorProps['prompts']
6+
Link: BlocksEditorProps['Link']
7+
}
8+
9+
const BlocksEditorContext = createContext<IBlocksProvider | undefined>(
10+
undefined,
11+
)
12+
13+
export function BlocksEditorProvider({
14+
children,
15+
Link,
16+
prompts,
17+
}: {
18+
children: ReactNode
19+
prompts: BlocksEditorProps['prompts']
20+
Link: BlocksEditorProps['Link']
21+
}) {
22+
const value = useRef<IBlocksProvider>({ prompts, Link })
23+
return (
24+
<BlocksEditorContext.Provider value={value.current}>
25+
{children}
26+
</BlocksEditorContext.Provider>
27+
)
28+
}
29+
30+
export function useBlocksEditorContext() {
31+
const context = useContext(BlocksEditorContext)
32+
33+
if (!context) {
34+
throw new Error(
35+
'useBlocksEditorContext must be used within an BlocksEditorProvider',
36+
)
37+
}
38+
return context
39+
}

packages/web-ui/src/ds/molecules/BlocksEditor/Editor/index.tsx

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,15 @@ import { InsertEmptyLinePlugin } from './plugins/InsertEmptyLinePlugin'
3434
import { VariableTransformPlugin } from './plugins/VariableTransformPlugin'
3535
import { VariableNode } from './nodes/VariableNode'
3636
import { VariableMenuPlugin } from './plugins/VariablesMenuPlugin'
37+
import { ReferencesPlugin } from './plugins/ReferencesPlugin'
38+
import { ReferenceNode } from './nodes/ReferenceNode'
39+
import { font } from '../../../tokens'
40+
import { BlocksEditorProvider } from './Provider'
3741

3842
const theme = {
3943
ltr: 'ltr',
4044
rtl: 'rtl',
41-
paragraph: 'block-paragraph text-sm leading-relaxed',
45+
paragraph: cn('block-paragraph align-middle', font.size.h5),
4246
text: {
4347
bold: 'font-bold',
4448
italic: 'italic',
@@ -68,7 +72,8 @@ function OnChangeHandler({
6872
onChange(textContent)
6973
}
7074

71-
// FIXME: Move to a separate function to handle blocks conversion
75+
// Move to a separate function to handle blocks conversion
76+
// This can be tested
7277
if (onBlocksChange) {
7378
const root = $getRoot()
7479
const blocks: AnyBlock[] = []
@@ -177,6 +182,10 @@ export function BlocksEditor({
177182
onChange,
178183
onBlocksChange,
179184
className,
185+
prompts,
186+
onRequestPromptMetadata,
187+
onToggleDevEditor,
188+
Link,
180189
readOnly = false,
181190
autoFocus = false,
182191
}: BlocksEditorProps) {
@@ -196,68 +205,75 @@ export function BlocksEditor({
196205
console.error('Editor error:', error)
197206
},
198207
editable: !readOnly,
199-
nodes: [MessageBlockNode, StepBlockNode, VariableNode],
208+
nodes: [MessageBlockNode, StepBlockNode, VariableNode, ReferenceNode],
200209
}
201210

202211
return (
203-
<div
204-
className={cn(
205-
'relative border border-gray-200 rounded-lg bg-white',
206-
'focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500',
207-
className,
208-
)}
209-
>
210-
<LexicalComposer initialConfig={initialConfig}>
211-
<div className='relative' ref={onRef}>
212-
<RichTextPlugin
213-
contentEditable={
214-
<ContentEditable
215-
className={cn(
216-
'min-h-[300px] py-4 [&_>*]:px-4 outline-none resize-none text-sm leading-relaxed',
217-
'focus:outline-none',
218-
VERTICAL_SPACE_CLASS,
219-
{
220-
'cursor-default': readOnly,
221-
},
222-
)}
223-
aria-placeholder={placeholder}
224-
placeholder={
225-
<div className='absolute top-4 left-4 text-gray-400 pointer-events-none select-none'>
226-
{placeholder}
227-
</div>
228-
}
229-
/>
230-
}
231-
ErrorBoundary={LexicalErrorBoundary}
232-
/>
212+
<BlocksEditorProvider Link={Link} prompts={prompts}>
213+
<div
214+
className={cn(
215+
'relative border border-gray-200 rounded-lg bg-white',
216+
'focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500',
217+
className,
218+
)}
219+
>
220+
<LexicalComposer initialConfig={initialConfig}>
221+
<div className='relative' ref={onRef}>
222+
<RichTextPlugin
223+
contentEditable={
224+
<ContentEditable
225+
className={cn(
226+
'min-h-[300px] py-4 [&_>*]:px-4 outline-none resize-none text-sm leading-relaxed',
227+
'focus:outline-none',
228+
VERTICAL_SPACE_CLASS,
229+
{
230+
'cursor-default': readOnly,
231+
},
232+
)}
233+
aria-placeholder={placeholder}
234+
placeholder={
235+
<div className='absolute top-4 left-4 text-gray-400 pointer-events-none select-none'>
236+
{placeholder}
237+
</div>
238+
}
239+
/>
240+
}
241+
ErrorBoundary={LexicalErrorBoundary}
242+
/>
233243

234-
{/* Core Plugins */}
235-
<HistoryPlugin />
236-
<BlocksPlugin />
237-
<EnterKeyPlugin />
238-
<InsertEmptyLinePlugin />
239-
<StepNameEditPlugin />
240-
<TypeaheadMenuPlugin />
241-
<VariableMenuPlugin />
242-
<InitializeBlocksPlugin initialBlocks={initialValue} />
243-
<HierarchyValidationPlugin />
244-
<VariableTransformPlugin />
244+
{/* Core Plugins */}
245+
<HistoryPlugin />
246+
<BlocksPlugin />
247+
<EnterKeyPlugin />
248+
<InsertEmptyLinePlugin />
249+
<StepNameEditPlugin />
250+
<TypeaheadMenuPlugin />
251+
<VariableMenuPlugin />
252+
<ReferencesPlugin
253+
prompts={prompts}
254+
onRequestPromptMetadata={onRequestPromptMetadata}
255+
onToggleDevEditor={onToggleDevEditor}
256+
/>
257+
<InitializeBlocksPlugin initialBlocks={initialValue} />
258+
<HierarchyValidationPlugin />
259+
<VariableTransformPlugin />
245260

246-
{/* Drag and Drop Plugin */}
247-
{!readOnly && floatingAnchorElem && (
248-
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
249-
)}
261+
{/* Drag and Drop Plugin */}
262+
{!readOnly && floatingAnchorElem && (
263+
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
264+
)}
250265

251-
{/* Auto focus */}
252-
{autoFocus && <AutoFocusPlugin />}
266+
{/* Auto focus */}
267+
{autoFocus && <AutoFocusPlugin />}
253268

254-
{/* Change handler */}
255-
<OnChangeHandler
256-
onChange={onChange}
257-
onBlocksChange={onBlocksChange}
258-
/>
259-
</div>
260-
</LexicalComposer>
261-
</div>
269+
{/* Change handler */}
270+
<OnChangeHandler
271+
onChange={onChange}
272+
onBlocksChange={onBlocksChange}
273+
/>
274+
</div>
275+
</LexicalComposer>
276+
</div>
277+
</BlocksEditorProvider>
262278
)
263279
}

0 commit comments

Comments
 (0)